From 5a463b320cf2ec15a9d3a0a21dc5f11e929f68b3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 11:24:35 +0100 Subject: [PATCH 001/188] docs: update frontend plugin docs to default to new frontend system Restructure plugin documentation so that the new frontend system is the default, unlabeled installation path. Old frontend system instructions are moved to a dedicated "Old Frontend System" section. Add a new "Installing Plugins" page to the frontend system docs covering package discovery, manual installation, and configuration. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../building-apps/05-installing-plugins.md | 67 ++++++++++ docs/getting-started/homepage.md | 52 +++----- microsite/sidebars.ts | 1 + plugins/home/README.md | 114 +++++++----------- plugins/mui-to-bui/README.md | 42 ++----- 5 files changed, 135 insertions(+), 141 deletions(-) create mode 100644 docs/frontend-system/building-apps/05-installing-plugins.md 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..bb78214382 --- /dev/null +++ b/docs/frontend-system/building-apps/05-installing-plugins.md @@ -0,0 +1,67 @@ +--- +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 [package discovery](#package-discovery), the plugin will be automatically detected and installed in the app. No additional code changes are needed. + +## Package discovery + +Package 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' +``` + +Package discovery requires that your app is built using the `@backstage/cli`, which is the default for all Backstage apps. + +## Manual installation + +If your app does not have [package discovery](#package-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 package discovery without causing conflicts. + +## 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/getting-started/homepage.md b/docs/getting-started/homepage.md index 05e422610a..7085f9b082 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -24,39 +24,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 package 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 +48,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,9 +66,9 @@ 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. @@ -98,17 +76,17 @@ The New Frontend System provides powerful customization options: 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 +## Old Frontend System -If your Backstage app uses the legacy frontend system, follow these steps: +This section covers how to set up the home plugin if your Backstage app still uses the old frontend system. -#### 1. Install the plugin +### 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 +### 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 @@ -119,7 +97,7 @@ export const HomePage = () => ( ); ``` -#### 3. Update router for the root `/` route +### 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. @@ -156,7 +134,7 @@ const routes = ( ); ``` -#### 4. Update sidebar items +### 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. @@ -206,13 +184,13 @@ That's it! You should now have _(although slightly boring)_ a homepage! In the next steps, we will make it interesting and useful! -### Use the default template +#### 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 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, diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 7e4e41591b..cf893d6895 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -597,6 +597,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/plugins/home/README.md b/plugins/home/README.md index 04d1b8c2ec..e75f28e6ed 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 package 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/mui-to-bui/README.md b/plugins/mui-to-bui/README.md index d964b4827f..d5f08e6d03 100644 --- a/plugins/mui-to-bui/README.md +++ b/plugins/mui-to-bui/README.md @@ -6,19 +6,22 @@ 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 package 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 -Add a route for the page in your app: +- 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, add a route for the page in your app: ```tsx // packages/app/src/App.tsx @@ -34,30 +37,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. From c202cd698ae4bc3899e1c4ca130c07ecb323d347 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 11:35:12 +0100 Subject: [PATCH 002/188] docs(catalog): update README for new frontend system as default Move old frontend system wiring instructions to an "Old Frontend System" section. The default installation path now uses package discovery with no manual wiring needed. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- plugins/catalog/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/catalog/README.md b/plugins/catalog/README.md index e8de1118ac..993f0e34f2 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 package 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. + ### Add the plugin to your `packages/app` Add the two pages that the catalog plugin provides to your app. You can choose From 5f80ce482d249c614ff6c43082cd350fe9012fd1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 11:35:40 +0100 Subject: [PATCH 003/188] docs(catalog-import): update README for new frontend system as default Move old frontend system wiring instructions to an "Old Frontend System" section. The default installation path now uses package discovery with no manual wiring needed. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- plugins/catalog-import/README.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/plugins/catalog-import/README.md b/plugins/catalog-import/README.md index d4ede8af7b..f8174b63ff 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 package 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,18 @@ Following React components accept optional props for providing custom example en /> ``` +## Old Frontend System + +If your Backstage app uses the old frontend system, 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. From 2082c44b4539806ed2b4f8850f7e8e3651cf50b0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 11:36:12 +0100 Subject: [PATCH 004/188] docs(scaffolder): update README for new frontend system as default Move old frontend system wiring instructions to an "Old Frontend System" section. The default installation path now uses package discovery with no manual wiring needed. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- plugins/scaffolder/README.md | 110 ++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/plugins/scaffolder/README.md b/plugins/scaffolder/README.md index 0a79d8afc0..ff09796033 100644 --- a/plugins/scaffolder/README.md +++ b/plugins/scaffolder/README.md @@ -15,13 +15,68 @@ 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 package 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 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 ! + +## Old Frontend System + +If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app. + ### Add the plugin to your `packages/app` Add the root page that the scaffolder plugin provides to your app. You can @@ -78,57 +133,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) From 348141b8ee8e587bf44ceee4236ee7e0a3a88b1d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 11:36:50 +0100 Subject: [PATCH 005/188] docs(user-settings): update README for new frontend system as default Move old frontend system wiring instructions to an "Old Frontend System" section. The default installation path now uses package discovery with no manual wiring needed. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- plugins/user-settings/README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index a27e5ca524..b4c283d275 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 package 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. + +### Components Usage Add the item to the Sidebar: From 96662b763ef492ea0e5c3ff2e86e13fff477306e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 11:37:33 +0100 Subject: [PATCH 006/188] docs(devtools): update README for new frontend system as default Move old frontend system wiring instructions to an "Old Frontend System" section. The default installation path now uses package discovery with no manual wiring needed. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- plugins/devtools/README.md | 76 +++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/plugins/devtools/README.md b/plugins/devtools/README.md index b23c35d4dd..54aa1f492d 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 package 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. + +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. From ae71853b45b393cc104e0fba5f7ecc60acb3131c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 11:38:40 +0100 Subject: [PATCH 007/188] docs(api-docs): update README for new frontend system as default Add new frontend system installation as the default path with package discovery. Move old frontend system wiring to an "Old Frontend System" section. Update README-alpha.md to be an extension reference rather than experimental documentation. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- plugins/api-docs/README-alpha.md | 6 +- plugins/api-docs/README.md | 151 ++++++++++++++++++------------- 2 files changed, 88 insertions(+), 69 deletions(-) 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..f6f52cbcae 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 package 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. + +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) From 7a2761b7ce262a6105a11b7e756056facd789cdd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 11:39:29 +0100 Subject: [PATCH 008/188] docs(catalog-graph): update README for new frontend system as default Add new frontend system installation as the default path with package discovery. Move old frontend system wiring to an "Old Frontend System" section. Update README-alpha.md to be an extension reference rather than experimental documentation. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- plugins/catalog-graph/README-alpha.md | 6 +- plugins/catalog-graph/README.md | 112 ++++++++++++++------------ 2 files changed, 64 insertions(+), 54 deletions(-) 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..38ea902271 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 package 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. + +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. From 8c4a718067de3a50ba9d68f2f4a7850d358f1d0b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 11:40:04 +0100 Subject: [PATCH 009/188] docs(org): update README for new frontend system as default Add new frontend system installation as the default path with package discovery and extension configuration. Move old frontend system wiring to an "Old Frontend System" section. Update README-alpha.md to be an extension reference rather than experimental documentation. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- plugins/org/README-alpha.md | 6 ++---- plugins/org/README.md | 30 +++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) 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..edf7cc4a89 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 package 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. + ### MyGroupsSidebarItem The MyGroupsSidebarItem provides quick access to the group(s) the logged in user is a member of directly in the sidebar. From db7d147d175dba9df7429a8f83a9977c0707c573 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 11:40:38 +0100 Subject: [PATCH 010/188] docs(catalog-unprocessed-entities): update README for new frontend system as default Make package discovery the default installation path. Move old frontend system wiring to an "Old Frontend System" section and remove the separate "Integrating with the New Frontend System" section. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../catalog-unprocessed-entities/README.md | 55 ++++++------------- 1 file changed, 16 insertions(+), 39 deletions(-) diff --git a/plugins/catalog-unprocessed-entities/README.md b/plugins/catalog-unprocessed-entities/README.md index b39a4a7ad4..13bca8b04f 100644 --- a/plugins/catalog-unprocessed-entities/README.md +++ b/plugins/catalog-unprocessed-entities/README.md @@ -32,7 +32,22 @@ 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 package 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, import into your `App.tsx` and include into the `` component: ```tsx title="packages/app/src/App.tsx" import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unprocessed-entities'; @@ -44,44 +59,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: From adc075f55739bc478451fcb2604b7fc079dd3a0b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 12:07:40 +0100 Subject: [PATCH 011/188] docs(kubernetes): update README for new frontend system as default Add install command and package discovery blurb. Restructure so the extension configuration is the default setup path, removing the explicit "New Frontend System" labeling. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- plugins/kubernetes/README.md | 49 +++++++++++++----------------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/plugins/kubernetes/README.md b/plugins/kubernetes/README.md index e2c967902b..42410ca92d 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 explicitly added to 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 package 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. From 67723818b5d01eacd980bc661801a12185491e5d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 12:14:48 +0100 Subject: [PATCH 012/188] docs: improve cross-references for installing plugins page Update the Building Apps overview to reference the new installing plugins page instead of duplicating feature discovery content. Add cross-references between the installing plugins page, the architecture feature discovery docs, the plugin conversion page, and the extension configuration page. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- docs/frontend-system/architecture/10-app.md | 2 ++ docs/frontend-system/building-apps/01-index.md | 10 +++------- .../building-apps/05-installing-plugins.md | 6 +++++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/frontend-system/architecture/10-app.md b/docs/frontend-system/architecture/10-app.md index 0303dd5e70..b9cbedc025 100644 --- a/docs/frontend-system/architecture/10-app.md +++ b/docs/frontend-system/architecture/10-app.md @@ -42,6 +42,8 @@ A common type of data that is shared between extensions is React elements and co ## Feature Discovery +For a practical guide on how to install plugins in your app, see [Installing Plugins](../building-apps/05-installing-plugins.md). + App feature discovery lets you automatically discover and install features provided by dependencies in your app. In practice, it means that you don't need to manually `import` features in code, but they are instead installed as soon as you add them as a dependency in your `package.json`. 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. diff --git a/docs/frontend-system/building-apps/01-index.md b/docs/frontend-system/building-apps/01-index.md index 8610dfb7b5..7a9f792f45 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 package 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 [package discovery](./05-installing-plugins.md#package-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 index bb78214382..72eeb465b0 100644 --- a/docs/frontend-system/building-apps/05-installing-plugins.md +++ b/docs/frontend-system/building-apps/05-installing-plugins.md @@ -43,7 +43,9 @@ app: - '@backstage/plugin-catalog' ``` -Package discovery requires that your app is built using the `@backstage/cli`, which is the default for all Backstage apps. +Package 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 package discovery works under the hood, see the [Feature Discovery](../architecture/10-app.md#feature-discovery) architecture documentation. ## Manual installation @@ -62,6 +64,8 @@ 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 package 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. From 17dff0ba255c009c32cc6d85140f4fc3c7ef8f22 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 12:43:03 +0100 Subject: [PATCH 013/188] docs: rename package discovery to feature discovery and reduce duplication Rename "package discovery" to "feature discovery" across all plugin READMEs and docs to match the established terminology. Slim down the Feature Discovery section in the architecture docs to avoid duplicating the configuration details now covered in the installing plugins page. Signed-off-by: Patrik Oldsberg Made-with: Cursor Signed-off-by: Patrik Oldsberg Made-with: Cursor --- docs/frontend-system/architecture/10-app.md | 28 +------------------ .../frontend-system/building-apps/01-index.md | 4 +-- .../building-apps/05-installing-plugins.md | 14 +++++----- docs/getting-started/homepage.md | 2 +- plugins/api-docs/README.md | 2 +- plugins/catalog-graph/README.md | 2 +- plugins/catalog-import/README.md | 2 +- .../catalog-unprocessed-entities/README.md | 2 +- plugins/catalog/README.md | 2 +- plugins/devtools/README.md | 2 +- plugins/home/README.md | 2 +- plugins/kubernetes/README.md | 2 +- plugins/mui-to-bui/README.md | 2 +- plugins/org/README.md | 2 +- plugins/scaffolder/README.md | 2 +- plugins/user-settings/README.md | 2 +- 16 files changed, 23 insertions(+), 49 deletions(-) diff --git a/docs/frontend-system/architecture/10-app.md b/docs/frontend-system/architecture/10-app.md index b9cbedc025..22457bcb30 100644 --- a/docs/frontend-system/architecture/10-app.md +++ b/docs/frontend-system/architecture/10-app.md @@ -42,37 +42,11 @@ A common type of data that is shared between extensions is React elements and co ## Feature Discovery -For a practical guide on how to install plugins in your app, see [Installing Plugins](../building-apps/05-installing-plugins.md). - App feature discovery lets you automatically discover and install features provided by dependencies in your app. In practice, it means that you don't need to manually `import` features in code, but they are instead installed as soon as you add them as a dependency in your `package.json`. 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/building-apps/01-index.md b/docs/frontend-system/building-apps/01-index.md index 7a9f792f45..bd9a0fbae5 100644 --- a/docs/frontend-system/building-apps/01-index.md +++ b/docs/frontend-system/building-apps/01-index.md @@ -65,7 +65,7 @@ Linking routes from different plugins requires this configuration. You can do th ### Install plugins -Plugins are typically installed by adding them as dependencies of your app package and relying on package 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). +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 @@ -79,7 +79,7 @@ Previously you would customize the application routes, components, apis, sidebar ### Install features manually -Most plugins are installed automatically through [package discovery](./05-installing-plugins.md#package-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: +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 index 72eeb465b0..9007ce0eb1 100644 --- a/docs/frontend-system/building-apps/05-installing-plugins.md +++ b/docs/frontend-system/building-apps/05-installing-plugins.md @@ -15,11 +15,11 @@ To install a plugin, add it as a dependency to your app package. For example, to yarn --cwd packages/app add @backstage/plugin-catalog ``` -If your app is set up with [package discovery](#package-discovery), the plugin will be automatically detected and installed in the app. No additional code changes are needed. +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. -## Package discovery +## Feature discovery -Package 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`: +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: @@ -43,13 +43,13 @@ app: - '@backstage/plugin-catalog' ``` -Package 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. +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 package discovery works under the hood, see the [Feature Discovery](../architecture/10-app.md#feature-discovery) architecture documentation. +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 [package discovery](#package-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`: +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'; @@ -62,7 +62,7 @@ const app = createApp({ 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 package discovery without causing conflicts. +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. diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index 7085f9b082..af39a825d8 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -32,7 +32,7 @@ Now, let's get started by installing the home plugin and creating a simple homep yarn --cwd packages/app add @backstage/plugin-home ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](../frontend-system/building-apps/05-installing-plugins.md). +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). ### 2. Configure the homepage as your root route diff --git a/plugins/api-docs/README.md b/plugins/api-docs/README.md index f6f52cbcae..5650b378f5 100644 --- a/plugins/api-docs/README.md +++ b/plugins/api-docs/README.md @@ -30,7 +30,7 @@ To link that a component provides or consumes an API, see the [`providesApis`](h yarn --cwd packages/app add @backstage/plugin-api-docs ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-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). You can enable entity cards and tabs on the catalog entity page through configuration: diff --git a/plugins/catalog-graph/README.md b/plugins/catalog-graph/README.md index 38ea902271..b1af843190 100644 --- a/plugins/catalog-graph/README.md +++ b/plugins/catalog-graph/README.md @@ -27,7 +27,7 @@ The plugin comes with these features: yarn --cwd packages/app add @backstage/plugin-catalog-graph ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-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). To enable the entity relations graph card on the catalog entity page, add the following configuration: diff --git a/plugins/catalog-import/README.md b/plugins/catalog-import/README.md index f8174b63ff..ba98d19f0b 100644 --- a/plugins/catalog-import/README.md +++ b/plugins/catalog-import/README.md @@ -22,7 +22,7 @@ Install the Catalog Import Plugin: yarn --cwd packages/app add @backstage/plugin-catalog-import ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-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). ## Customizations diff --git a/plugins/catalog-unprocessed-entities/README.md b/plugins/catalog-unprocessed-entities/README.md index 13bca8b04f..8cdf275851 100644 --- a/plugins/catalog-unprocessed-entities/README.md +++ b/plugins/catalog-unprocessed-entities/README.md @@ -32,7 +32,7 @@ Requires the `@backstage/plugin-catalog-backend-module-unprocessed` module to be yarn --cwd packages/app add @backstage/plugin-catalog-unprocessed-entities ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-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). You can optionally add unprocessed entities as a tab in DevTools through configuration: diff --git a/plugins/catalog/README.md b/plugins/catalog/README.md index 993f0e34f2..ae8ddd1033 100644 --- a/plugins/catalog/README.md +++ b/plugins/catalog/README.md @@ -20,7 +20,7 @@ plugin, if you previously removed it. yarn --cwd packages/app add @backstage/plugin-catalog ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-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). ## Old Frontend System diff --git a/plugins/devtools/README.md b/plugins/devtools/README.md index 54aa1f492d..b049839861 100644 --- a/plugins/devtools/README.md +++ b/plugins/devtools/README.md @@ -73,7 +73,7 @@ Install the `@backstage/plugin-devtools` package in your frontend app: yarn --cwd packages/app add @backstage/plugin-devtools ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-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). ## Customizing diff --git a/plugins/home/README.md b/plugins/home/README.md index e75f28e6ed..2c46530c3d 100644 --- a/plugins/home/README.md +++ b/plugins/home/README.md @@ -11,7 +11,7 @@ For App Integrators, the system is designed to be composable to give total freed yarn --cwd packages/app add @backstage/plugin-home ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-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: diff --git a/plugins/kubernetes/README.md b/plugins/kubernetes/README.md index 42410ca92d..baf0ab42a3 100644 --- a/plugins/kubernetes/README.md +++ b/plugins/kubernetes/README.md @@ -21,7 +21,7 @@ This plugin must be explicitly added to a Backstage app, along with its peer bac yarn --cwd packages/app add @backstage/plugin-kubernetes ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-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). It requires configuration in the Backstage `app-config.yaml` to connect to a Kubernetes API control plane. diff --git a/plugins/mui-to-bui/README.md b/plugins/mui-to-bui/README.md index d5f08e6d03..4217f103a8 100644 --- a/plugins/mui-to-bui/README.md +++ b/plugins/mui-to-bui/README.md @@ -12,7 +12,7 @@ Add the dependency to your app: yarn --cwd packages/app add @backstage/plugin-mui-to-bui ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-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). ## Accessing the Themer page diff --git a/plugins/org/README.md b/plugins/org/README.md index edf7cc4a89..9c8050e940 100644 --- a/plugins/org/README.md +++ b/plugins/org/README.md @@ -25,7 +25,7 @@ Here's an example of what the User Profile looks like: yarn --cwd packages/app add @backstage/plugin-org ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-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). You can enable entity cards on the catalog entity page through configuration: diff --git a/plugins/scaffolder/README.md b/plugins/scaffolder/README.md index ff09796033..6a2ad697b5 100644 --- a/plugins/scaffolder/README.md +++ b/plugins/scaffolder/README.md @@ -20,7 +20,7 @@ the plugin, if you previously removed it. yarn --cwd packages/app add @backstage/plugin-scaffolder ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-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). ### Troubleshooting diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index b4c283d275..2fa5e5db39 100644 --- a/plugins/user-settings/README.md +++ b/plugins/user-settings/README.md @@ -18,7 +18,7 @@ for installation instructions. yarn --cwd packages/app add @backstage/plugin-user-settings ``` -Once installed, the plugin is automatically available in your app through the default package discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-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). ## Old Frontend System From 538c985672b77133dfcfecce0fbdc434242856b0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 13:43:47 +0100 Subject: [PATCH 014/188] Add changesets for plugin README updates Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/docs-frontend-plugin-installation.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .changeset/docs-frontend-plugin-installation.md diff --git a/.changeset/docs-frontend-plugin-installation.md b/.changeset/docs-frontend-plugin-installation.md new file mode 100644 index 0000000000..8a13f5c12d --- /dev/null +++ b/.changeset/docs-frontend-plugin-installation.md @@ -0,0 +1,16 @@ +--- +'@backstage/plugin-api-docs': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog-unprocessed-entities': patch +'@backstage/plugin-devtools': patch +'@backstage/plugin-home': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-mui-to-bui': patch +'@backstage/plugin-org': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-user-settings': patch +--- + +Updated installation documentation to use feature discovery as the default. From 2a278818ffc591e1c8c7d71ad3446138df761b40 Mon Sep 17 00:00:00 2001 From: abdellahhanane Date: Wed, 4 Mar 2026 22:37:10 -0500 Subject: [PATCH 015/188] feat(scaffolder): add returnWorkflowRunDetails to github:actions:dispatch action Signed-off-by: abdellahhanane --- .../report.api.md | 9 +- .../githubActionsDispatch.examples.test.ts | 41 ++++++++++ .../actions/githubActionsDispatch.examples.ts | 17 ++++ .../src/actions/githubActionsDispatch.test.ts | 62 ++++++++++++++ .../src/actions/githubActionsDispatch.ts | 82 +++++++++++++++---- 5 files changed, 193 insertions(+), 18 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index b6b2d97ac8..3538529135 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' >; @@ -250,8 +253,8 @@ export function createGithubRepoCreateAction(options: { access: string; } | { - team: string; access: string; + team: string; } )[] | undefined; @@ -441,8 +444,8 @@ export function createPublishGithubAction(options: { access: string; } | { - team: string; access: string; + team: string; } )[] | undefined; 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..b917efbf28 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 @@ -93,6 +93,7 @@ describe('github:actions:dispatch', () => { ).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', + return_run_details: false, workflow_id: workflowId, ref: branchOrTagName, }); @@ -119,9 +120,49 @@ describe('github:actions:dispatch', () => { ).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', + return_run_details: false, workflow_id: workflowId, ref: branchOrTagName, 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..67f0cf3e82 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts @@ -101,6 +101,7 @@ describe('github:actions:dispatch', () => { repo: 'repo', workflow_id: workflowId, ref: branchOrTagName, + return_run_details: false, }); }); @@ -128,6 +129,67 @@ describe('github:actions:dispatch', () => { workflow_id: workflowId, ref: branchOrTagName, inputs: workflowInputs, + return_run_details: false, }); }); + + 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..cc2f5f13e8 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,56 @@ 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 response = await client.rest.actions.createWorkflowDispatch({ + owner, + repo, + workflow_id: workflowId, + ref: branchOrTagName, + inputs: workflowInputs, + return_run_details: returnWorkflowRunDetails ?? false, + }); - 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 { + workflow_run_id: data.workflow_run_id, + run_url: data.run_url, + workflowRunHtmlUrl: data.html_url, + }; + } + return null; + }, + }); + + if (runDetails) { + ctx.output('workflowRunId', runDetails.workflow_run_id); + ctx.output('workflowRunUrl', runDetails.run_url); + ctx.output('workflowRunHtmlUrl', runDetails.workflowRunHtmlUrl); + } + } catch (e) { + assertError(e); + ctx.logger.warn( + `Failed: dispatching workflow '${workflowId}' on repo: '${repo}', ${e.message}`, + ); + throw e; + } }, }); } From a761a483314ee6a54f33f565d0e2388b732fb4cb Mon Sep 17 00:00:00 2001 From: abdellahhanane Date: Wed, 4 Mar 2026 22:46:49 -0500 Subject: [PATCH 016/188] add changelot Signed-off-by: abdellahhanane --- .changeset/few-clouds-pull.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/few-clouds-pull.md diff --git a/.changeset/few-clouds-pull.md b/.changeset/few-clouds-pull.md new file mode 100644 index 0000000000..52c338ac63 --- /dev/null +++ b/.changeset/few-clouds-pull.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added optional returnWorkflowRunDetails input to github:actions:dispatch action. When true, exposes workflowRunId, workflowRunUrl, and workflowRunHtmlUrl as outputs using the GitHub API return_run_details parameter. From df21ef6330edda8a6997a1163811a313f441b8ec Mon Sep 17 00:00:00 2001 From: abdellahhanane Date: Thu, 5 Mar 2026 08:21:35 -0500 Subject: [PATCH 017/188] Apply copilot comments and fix CI Signed-off-by: abdellahhanane --- .../githubActionsDispatch.examples.test.ts | 2 -- .../src/actions/githubActionsDispatch.test.ts | 2 -- .../src/actions/githubActionsDispatch.ts | 17 ++++++++++------- 3 files changed, 10 insertions(+), 11 deletions(-) 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 b917efbf28..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 @@ -93,7 +93,6 @@ describe('github:actions:dispatch', () => { ).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', - return_run_details: false, workflow_id: workflowId, ref: branchOrTagName, }); @@ -120,7 +119,6 @@ describe('github:actions:dispatch', () => { ).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', - return_run_details: false, workflow_id: workflowId, ref: branchOrTagName, inputs: workflowInputs, 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 67f0cf3e82..fc8cfcce0b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts @@ -101,7 +101,6 @@ describe('github:actions:dispatch', () => { repo: 'repo', workflow_id: workflowId, ref: branchOrTagName, - return_run_details: false, }); }); @@ -129,7 +128,6 @@ describe('github:actions:dispatch', () => { workflow_id: workflowId, ref: branchOrTagName, inputs: workflowInputs, - return_run_details: false, }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts index cc2f5f13e8..c882869085 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts @@ -126,14 +126,17 @@ export function createGithubActionsDispatchAction(options: { const runDetails = await ctx.checkpoint({ key: `create.workflow.dispatch.${owner}.${repo}.${workflowId}`, fn: async () => { - const response = await client.rest.actions.createWorkflowDispatch({ + const dispatchParams = { owner, repo, workflow_id: workflowId, ref: branchOrTagName, inputs: workflowInputs, - return_run_details: returnWorkflowRunDetails ?? false, - }); + ...(returnWorkflowRunDetails ? { return_run_details: true } : {}), + }; + const response = await client.rest.actions.createWorkflowDispatch( + dispatchParams, + ); ctx.logger.info(`Workflow ${workflowId} dispatched successfully`); @@ -151,8 +154,8 @@ export function createGithubActionsDispatchAction(options: { }; return { - workflow_run_id: data.workflow_run_id, - run_url: data.run_url, + workflowRunId: data.workflow_run_id, + workflowRunUrl: data.run_url, workflowRunHtmlUrl: data.html_url, }; } @@ -161,8 +164,8 @@ export function createGithubActionsDispatchAction(options: { }); if (runDetails) { - ctx.output('workflowRunId', runDetails.workflow_run_id); - ctx.output('workflowRunUrl', runDetails.run_url); + ctx.output('workflowRunId', runDetails.workflowRunId); + ctx.output('workflowRunUrl', runDetails.workflowRunUrl); ctx.output('workflowRunHtmlUrl', runDetails.workflowRunHtmlUrl); } } catch (e) { From fba21cfbcbcb2e61211c377ea498056328ddd43f Mon Sep 17 00:00:00 2001 From: abdellahhanane Date: Thu, 5 Mar 2026 08:22:56 -0500 Subject: [PATCH 018/188] Add return_run_details to accept.txt Signed-off-by: abdellahhanane --- .github/vale/config/vocabularies/Backstage/accept.txt | 1 + plugins/scaffolder-backend-module-github/report.api.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 11df7fc1a0..cbf3130537 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -581,3 +581,4 @@ resizable enums LLMs cleye +return_run_details diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 3538529135..9e930a7866 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -253,8 +253,8 @@ export function createGithubRepoCreateAction(options: { access: string; } | { - access: string; team: string; + access: string; } )[] | undefined; @@ -444,8 +444,8 @@ export function createPublishGithubAction(options: { access: string; } | { - access: string; team: string; + access: string; } )[] | undefined; From 4f4699525dc763cc9b998d96fcb7c024465d23f6 Mon Sep 17 00:00:00 2001 From: dev404ai Date: Fri, 6 Mar 2026 12:16:59 +0700 Subject: [PATCH 019/188] Add RuleHub Policy Catalog plugin directory entry Signed-off-by: dev404ai --- microsite/data/plugins/rulehub-policy-catalog.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 microsite/data/plugins/rulehub-policy-catalog.yaml 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 From 0670f17050325f78ae361000ee809ec87d6459d5 Mon Sep 17 00:00:00 2001 From: divyamagrawal06 Date: Sun, 8 Mar 2026 03:47:09 +0530 Subject: [PATCH 020/188] fix(microsite): add missing meta description to homepage Signed-off-by: divyamagrawal06 --- .changeset/seo-meta-fix.md | 5 +++++ microsite/docusaurus.config.ts | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/seo-meta-fix.md diff --git a/.changeset/seo-meta-fix.md b/.changeset/seo-meta-fix.md new file mode 100644 index 0000000000..bb6f11c100 --- /dev/null +++ b/.changeset/seo-meta-fix.md @@ -0,0 +1,5 @@ +--- +"@backstage/microsite": patch +--- + +Add missing meta description to the backstage.io homepage to ensure stable search engine indexing and prevent raw asset path leakage in SERP snippets. \ No newline at end of file diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index ce75a2e063..7b1647809b 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -450,6 +450,12 @@ const config: Config = { ], }, image: 'img/sharing-opengraph.png', + metadata: [ + { + name: 'description', + content: 'Backstage is an open source framework for building developer portals. ', + }, + ], footer: { links: [ { From 51c63d53cf57729b6fe0fca63562beb90d239fb6 Mon Sep 17 00:00:00 2001 From: Divyam Agrawal Date: Sun, 8 Mar 2026 04:25:42 +0530 Subject: [PATCH 021/188] Update microsite/docusaurus.config.ts Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: Divyam Agrawal --- microsite/docusaurus.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index 7b1647809b..dbdb6c2710 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -453,7 +453,7 @@ const config: Config = { metadata: [ { name: 'description', - content: 'Backstage is an open source framework for building developer portals. ', + 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: { From e1b57131994db677cef2115bd12fc622f34caa2f Mon Sep 17 00:00:00 2001 From: divyamagrawal06 Date: Sun, 8 Mar 2026 04:27:57 +0530 Subject: [PATCH 022/188] chore: remove unnecessary changeset for microsite Signed-off-by: divyamagrawal06 --- .changeset/seo-meta-fix.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/seo-meta-fix.md diff --git a/.changeset/seo-meta-fix.md b/.changeset/seo-meta-fix.md deleted file mode 100644 index bb6f11c100..0000000000 --- a/.changeset/seo-meta-fix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@backstage/microsite": patch ---- - -Add missing meta description to the backstage.io homepage to ensure stable search engine indexing and prevent raw asset path leakage in SERP snippets. \ No newline at end of file From 844f7ba3ab055ae6a00ae9aef2cf298ee85189b6 Mon Sep 17 00:00:00 2001 From: divyamagrawal06 Date: Sun, 8 Mar 2026 14:38:12 +0530 Subject: [PATCH 023/188] chore: format as per prettier Signed-off-by: divyamagrawal06 --- microsite/docusaurus.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index dbdb6c2710..718a337b01 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -453,7 +453,8 @@ const config: Config = { 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.', + 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: { From ed5d1ea15c1da828c7dd0f87bd11c7e18ca9bedf Mon Sep 17 00:00:00 2001 From: abdellahhanane Date: Mon, 9 Mar 2026 10:29:10 -0400 Subject: [PATCH 024/188] Apply PR comments Signed-off-by: abdellahhanane --- .changeset/few-clouds-pull.md | 2 +- .github/vale/config/vocabularies/Backstage/accept.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.changeset/few-clouds-pull.md b/.changeset/few-clouds-pull.md index 52c338ac63..4f8a67389b 100644 --- a/.changeset/few-clouds-pull.md +++ b/.changeset/few-clouds-pull.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend-module-github': patch --- -Added optional returnWorkflowRunDetails input to github:actions:dispatch action. When true, exposes workflowRunId, workflowRunUrl, and workflowRunHtmlUrl as outputs using the GitHub API return_run_details parameter. +Added optional `returnWorkflowRunDetails` input to `github:actions:dispatch` action. When true, exposes `workflowRunId`, `workflowRunUrl`, and `workflowRunHtmlUrl` as outputs using the GitHub API `return_run_details` parameter. diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index cbf3130537..11df7fc1a0 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -581,4 +581,3 @@ resizable enums LLMs cleye -return_run_details From cd0ecc50b10c8ff74b044e06930f91ddb41815e1 Mon Sep 17 00:00:00 2001 From: Sander van Delden <38953576+SanderDelden@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:21:00 +0100 Subject: [PATCH 025/188] feat(plugin-scaffolder-node/githelpers): add function to stage files for removal Signed-off-by: Sander van Delden <38953576+SanderDelden@users.noreply.github.com> --- .changeset/cruel-cities-jump.md | 5 +++ plugins/scaffolder-node/report.api.md | 15 +++++++ .../src/actions/gitHelpers.test.ts | 43 +++++++++++++++++++ .../scaffolder-node/src/actions/gitHelpers.ts | 21 +++++++++ plugins/scaffolder-node/src/actions/index.ts | 1 + 5 files changed, 85 insertions(+) create mode 100644 .changeset/cruel-cities-jump.md diff --git a/.changeset/cruel-cities-jump.md b/.changeset/cruel-cities-jump.md new file mode 100644 index 0000000000..f37f8dcef1 --- /dev/null +++ b/.changeset/cruel-cities-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-node': patch +--- + +Added `removeFiles` helper function for staging file removals in Git. diff --git a/plugins/scaffolder-node/report.api.md b/plugins/scaffolder-node/report.api.md index 54282f370d..24b51ed989 100644 --- a/plugins/scaffolder-node/report.api.md +++ b/plugins/scaffolder-node/report.api.md @@ -318,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) 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'; From 132047b4c08035e11516c45141d166401c24b6b3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:10:06 +0000 Subject: [PATCH 026/188] chore(deps): update step-security/harden-runner action to v2.15.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/api-breaking-changes-comment.yml | 2 +- .github/workflows/api-breaking-changes.yml | 2 +- .github/workflows/automate_area-labels.yml | 2 +- .github/workflows/automate_changeset_feedback.yml | 2 +- .github/workflows/automate_merge_message.yml | 2 +- .github/workflows/automate_stale.yml | 2 +- .github/workflows/ci-noop.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/cron.yml | 2 +- .github/workflows/deploy_docker-image.yml | 2 +- .github/workflows/deploy_microsite.yml | 6 +++--- .github/workflows/deploy_packages.yml | 2 +- .github/workflows/issue.yaml | 2 +- .github/workflows/mui-migration-tracker.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/sync_bui-docs.yml | 2 +- .github/workflows/sync_code-formatting.yml | 2 +- .github/workflows/sync_dependabot-changesets.yml | 2 +- .github/workflows/sync_patch-release.yml | 2 +- .github/workflows/sync_pull-requests-scheduled.yml | 4 ++-- .github/workflows/sync_pull-requests-trigger.yml | 2 +- .github/workflows/sync_pull-requests.yml | 2 +- .github/workflows/sync_release-manifest.yml | 2 +- .github/workflows/sync_renovate-changesets.yml | 2 +- .github/workflows/sync_snyk-github-issues.yml | 2 +- .github/workflows/sync_snyk-monitor.yml | 2 +- .github/workflows/sync_version-packages.yml | 2 +- .github/workflows/verify_accessibility-noop.yml | 2 +- .github/workflows/verify_accessibility.yml | 2 +- .github/workflows/verify_chromatic-noop.yml | 2 +- .github/workflows/verify_chromatic.yml | 2 +- .github/workflows/verify_codeql.yml | 2 +- .github/workflows/verify_docs-quality.yml | 2 +- .github/workflows/verify_e2e-linux-noop.yml | 2 +- .github/workflows/verify_e2e-linux.yml | 2 +- .github/workflows/verify_e2e-techdocs.yml | 2 +- .github/workflows/verify_e2e-windows-noop.yml | 2 +- .github/workflows/verify_e2e-windows.yml | 2 +- .github/workflows/verify_fossa.yml | 2 +- .github/workflows/verify_microsite-noop.yml | 2 +- .github/workflows/verify_microsite.yml | 6 +++--- .github/workflows/verify_microsite_accessibility-noop.yml | 2 +- .github/workflows/verify_microsite_accessibility.yml | 2 +- .github/workflows/verify_windows.yml | 2 +- 44 files changed, 50 insertions(+), 50 deletions(-) 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 4425f81905..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 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 fadb3efb84..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 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 34ab03f207..4bed28af3a 100644 --- a/.github/workflows/automate_stale.yml +++ b/.github/workflows/automate_stale.yml @@ -15,7 +15,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/ci-noop.yml b/.github/workflows/ci-noop.yml index 0fca26a12c..71b0c68791 100644 --- a/.github/workflows/ci-noop.yml +++ b/.github/workflows/ci-noop.yml @@ -41,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 24854003fa..bb5474506d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,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 @@ -78,7 +78,7 @@ 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 diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index a9415b24e1..61f8a2ce51 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -10,7 +10,7 @@ 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 diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index 61841f4052..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 diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index f88f84adad..c05a14ffe6 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -23,7 +23,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 @@ -124,7 +124,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 @@ -218,7 +218,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/deploy_packages.yml b/.github/workflows/deploy_packages.yml index 601751c005..37e188a1f0 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -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 36107d33f2..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 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8f7c462785..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 diff --git a/.github/workflows/sync_bui-docs.yml b/.github/workflows/sync_bui-docs.yml index 15319c19b9..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 diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index 2ae13eaa25..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 diff --git a/.github/workflows/sync_dependabot-changesets.yml b/.github/workflows/sync_dependabot-changesets.yml index 8768aff21a..475e3ac568 100644 --- a/.github/workflows/sync_dependabot-changesets.yml +++ b/.github/workflows/sync_dependabot-changesets.yml @@ -11,7 +11,7 @@ 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 diff --git a/.github/workflows/sync_patch-release.yml b/.github/workflows/sync_patch-release.yml index 3e96529e53..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 diff --git a/.github/workflows/sync_pull-requests-scheduled.yml b/.github/workflows/sync_pull-requests-scheduled.yml index 210fc1b20a..3854a0f4df 100644 --- a/.github/workflows/sync_pull-requests-scheduled.yml +++ b/.github/workflows/sync_pull-requests-scheduled.yml @@ -19,7 +19,7 @@ jobs: count: ${{ steps.batch.outputs.count }} 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 @@ -70,7 +70,7 @@ jobs: max-parallel: 1 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-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 4d607aab13..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 diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index 0fee5f17b5..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 diff --git a/.github/workflows/sync_renovate-changesets.yml b/.github/workflows/sync_renovate-changesets.yml index 0940de9c55..04495e606b 100644 --- a/.github/workflows/sync_renovate-changesets.yml +++ b/.github/workflows/sync_renovate-changesets.yml @@ -11,7 +11,7 @@ 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 diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 4a067dbbd3..b04c5b9ab7 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -12,7 +12,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/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index 985ff6fe9a..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 diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index 6cdcddc8be..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 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 312cdd2903..83d27612ae 100644 --- a/.github/workflows/verify_accessibility.yml +++ b/.github/workflows/verify_accessibility.yml @@ -20,7 +20,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_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 13be85fe97..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 diff --git a/.github/workflows/verify_codeql.yml b/.github/workflows/verify_codeql.yml index 628577e09d..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 diff --git a/.github/workflows/verify_docs-quality.yml b/.github/workflows/verify_docs-quality.yml index 6ace7d72a3..792402f7ad 100644 --- a/.github/workflows/verify_docs-quality.yml +++ b/.github/workflows/verify_docs-quality.yml @@ -15,7 +15,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_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 4e0f82dd2c..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 diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index f3786f19c3..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 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 2b0e3d47c3..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 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 4fb6a98f3e..80adffbba2 100644 --- a/.github/workflows/verify_microsite.yml +++ b/.github/workflows/verify_microsite.yml @@ -28,7 +28,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 @@ -126,7 +126,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 @@ -212,7 +212,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_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 ff7a91c716..226f1f71be 100644 --- a/.github/workflows/verify_microsite_accessibility.yml +++ b/.github/workflows/verify_microsite_accessibility.yml @@ -15,7 +15,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_windows.yml b/.github/workflows/verify_windows.yml index 26657cbcd3..3e74e088ec 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.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 From 80c0b16e181c1e357227bd1a08b501eebf12ac16 Mon Sep 17 00:00:00 2001 From: Iury Lenon Alves Date: Fri, 13 Feb 2026 14:06:29 +0000 Subject: [PATCH 027/188] docs(integrations): add mention of backstage-openapi backend module Signed-off-by: Iury Lenon Alves --- docs/integrations/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/integrations/index.md b/docs/integrations/index.md index 00463a810f..89f8e044c3 100644 --- a/docs/integrations/index.md +++ b/docs/integrations/index.md @@ -26,3 +26,9 @@ integrations: See documentation for each type of integration for full details on configuration. + +## Backstage OpenAPI Module + +If you want to integrate the OpenAPI specifications from your Backstage instance itself into the catalog, you can use the `catalog-backend-module-backstage-openapi`. This module helps discover and ingest OpenAPI definitions from Backstage plugins. + +For more details, see the [module documentation](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-backstage-openapi). From f1bfd351cd9ef85497567c5f24231458b428cd08 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:10:33 +0000 Subject: [PATCH 028/188] chore(deps): update dependency @types/jquery to v4 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/app-legacy/package.json | 2 +- packages/app/package.json | 2 +- yarn.lock | 21 ++++++--------------- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/packages/app-legacy/package.json b/packages/app-legacy/package.json index 0de94c265a..30cdcdadb0 100644 --- a/packages/app-legacy/package.json +++ b/packages/app-legacy/package.json @@ -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/package.json b/packages/app/package.json index e79ba2775c..28e3c97235 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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/yarn.lock b/yarn.lock index 907385f781..363622e0ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22213,12 +22213,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 @@ -23094,13 +23092,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" @@ -31662,7 +31653,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" @@ -31743,7 +31734,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" From f75764c7179565b683139c2419bb926460fab396 Mon Sep 17 00:00:00 2001 From: Iury Lenon Alves Date: Mon, 23 Feb 2026 20:37:58 +0000 Subject: [PATCH 029/188] docs: move backstage openapi docs to configuration.md and add install steps Signed-off-by: Iury Lenon Alves --- docs/features/software-catalog/configuration.md | 6 ++++++ docs/integrations/index.md | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 8c9241eb36..140b186137 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -371,6 +371,12 @@ backend.add(eventsModuleCatalogErrors); 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. +## 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. Add the package to your backend: diff --git a/docs/integrations/index.md b/docs/integrations/index.md index 89f8e044c3..00463a810f 100644 --- a/docs/integrations/index.md +++ b/docs/integrations/index.md @@ -26,9 +26,3 @@ integrations: See documentation for each type of integration for full details on configuration. - -## Backstage OpenAPI Module - -If you want to integrate the OpenAPI specifications from your Backstage instance itself into the catalog, you can use the `catalog-backend-module-backstage-openapi`. This module helps discover and ingest OpenAPI definitions from Backstage plugins. - -For more details, see the [module documentation](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-backstage-openapi). From d4199a9d282b581019e86e5117f27aeb15450714 Mon Sep 17 00:00:00 2001 From: Iury Lenon Alves Date: Mon, 23 Feb 2026 21:44:36 +0000 Subject: [PATCH 030/188] docs: add required app-config for backstage-openapi module Signed-off-by: Iury Lenon Alves --- docs/features/software-catalog/configuration.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 140b186137..4c50719cb9 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -408,3 +408,14 @@ spec: definition: $openapi: ./spec/openapi.yaml # by using $openapi Backstage will now resolve all $ref instances ``` + +3. Add the configuration to your `app-config.yaml`: + +```yaml title="app-config.yaml" +catalog: + providers: + backstageOpenapi: + plugins: + - catalog + - scaffolder +``` From 346d3302c2781c25e82c20841f12558e20caa252 Mon Sep 17 00:00:00 2001 From: Iury Lenon Alves Date: Mon, 9 Mar 2026 22:52:05 +0000 Subject: [PATCH 031/188] docs: fix markdown hierarchy and apply copilot suggestions Signed-off-by: Iury Lenon Alves --- .../software-catalog/configuration.md | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 4c50719cb9..6ceb00b3b5 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -371,12 +371,6 @@ backend.add(eventsModuleCatalogErrors); 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. -## 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. Add the package to your backend: @@ -409,6 +403,28 @@ spec: $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" @@ -418,4 +434,14 @@ catalog: 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 ``` From 9599697048d17e818d6121229698cb1b232f42f2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 03:43:56 +0000 Subject: [PATCH 032/188] chore(deps): update dependency globals to v17 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-422217b.md | 5 +++++ packages/ui/package.json | 2 +- yarn.lock | 17 +++++------------ 3 files changed, 11 insertions(+), 13 deletions(-) create mode 100644 .changeset/renovate-422217b.md diff --git a/.changeset/renovate-422217b.md b/.changeset/renovate-422217b.md new file mode 100644 index 0000000000..fec1d46bfb --- /dev/null +++ b/.changeset/renovate-422217b.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Updated dependency `globals` to `^17.0.0`. diff --git a/packages/ui/package.json b/packages/ui/package.json index 1440a40831..c307ed7ae3 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -60,7 +60,7 @@ "@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/yarn.lock b/yarn.lock index 907385f781..7f6998e4c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8104,7 +8104,7 @@ __metadata: 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" @@ -33637,17 +33637,10 @@ __metadata: languageName: node linkType: hard -"globals@npm:^15.11.0": - version: 15.15.0 - resolution: "globals@npm:15.15.0" - checksum: 10/7f561c87b2fd381b27fc2db7df8a4ea7a9bb378667b8a7193e61fd2ca3a876479174e2a303a74345fbea6e1242e16db48915c1fd3bf35adcf4060a795b425e18 - languageName: node - linkType: hard - -"globals@npm:^17.3.0": - version: 17.3.0 - resolution: "globals@npm:17.3.0" - checksum: 10/44ba2b7db93eb6a2531dfba09219845e21f2e724a4f400eb59518b180b7d5bcf7f65580530e3d3023d7dc2bdbacf5d265fd87c393f567deb9a2b0472b51c9d5e +"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 From 5cdba6200d6d1a4caef7c406476ed4953113cade Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:22:41 +0000 Subject: [PATCH 033/188] chore(deps): update dependency lint-staged to v16 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 225 ++++++++++++++------------------------------------- 2 files changed, 63 insertions(+), 164 deletions(-) diff --git a/package.json b/package.json index d356805b05..cbcc97e5dc 100644 --- a/package.json +++ b/package.json @@ -163,7 +163,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", diff --git a/yarn.lock b/yarn.lock index 3dd82bf33e..41c1687167 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24770,10 +24770,10 @@ __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 @@ -26670,13 +26670,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" @@ -26989,13 +26982,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 @@ -27417,13 +27410,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^13.1.0": - version: 13.1.0 - resolution: "commander@npm:13.1.0" - checksum: 10/d3b4b79e6be8471ddadacbb8cd441fe82154d7da7393b50e76165a9e29ccdb74fa911a186437b9a211d0fc071db6051915c94fb8ef16d77511d898e9dbabc6af - languageName: node - linkType: hard - "commander@npm:^14.0.3": version: 14.0.3 resolution: "commander@npm:14.0.3" @@ -31078,23 +31064,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" @@ -32473,10 +32442,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 @@ -32578,13 +32547,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" @@ -34053,13 +34015,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" @@ -34837,19 +34792,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 @@ -35207,13 +35155,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" @@ -37213,13 +37154,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" @@ -37253,23 +37187,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 @@ -37280,17 +37210,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 @@ -39045,13 +38975,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" @@ -40535,15 +40458,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" @@ -40839,15 +40753,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" @@ -41809,13 +41714,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" @@ -42108,15 +42006,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" @@ -45672,7 +45561,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" @@ -46614,16 +46503,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" @@ -46634,6 +46513,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" @@ -47386,6 +47275,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" @@ -47513,7 +47412,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0": +"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: @@ -47578,13 +47477,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" @@ -48426,6 +48318,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" @@ -51137,7 +51036,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: From cbcc344945ec29e35871c6a525071cc10059a138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 10 Mar 2026 19:43:23 +0100 Subject: [PATCH 034/188] one more flake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../engine/IncrementalIngestionEngine.test.ts | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) 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..efcaa44dc1 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,18 +107,13 @@ 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 + // After 3 calls: time is 1120, elapsed is 120 > 100ms burst length + // The burst check happens after the 3rd call, so we get 3 calls expect(result).toBe(false); - expect(duration).toBeGreaterThanOrEqual(100); - expect(duration).toBeLessThan(200); expect(mockProvider.next).toHaveBeenCalledTimes(callCount); - expect(callCount).toBeGreaterThan(1); }); @@ -127,11 +135,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 +156,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 +181,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 +207,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); }); }); From 85ec4d731476e0590c35bf5ac45f4c63649af691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 10 Mar 2026 20:12:49 +0100 Subject: [PATCH 035/188] fix: remove tautological assertion in burst length test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the self-referencing callCount assertion with a concrete expected value of 3, derived from the mocked time progression. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- .../src/engine/IncrementalIngestionEngine.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 efcaa44dc1..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 @@ -110,11 +110,12 @@ describe('IncrementalIngestionEngine - Burst Length', () => { const result = await engine.ingestOneBurst('test-ingestion', signal); - // After 3 calls: time is 1120, elapsed is 120 > 100ms burst length - // The burst check happens after the 3rd call, so we get 3 calls + // 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(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 () => { From 70183e9c5555257abd229c540da1172bda9d1fa8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 11 Mar 2026 08:37:44 +0100 Subject: [PATCH 036/188] AGENTS.md: fix duplicate /packages/app entry in Key Directories Signed-off-by: Patrik Oldsberg Made-with: Cursor --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 001989d9b6..010ebca9e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,9 @@ Backstage is an open platform for building developer portals. This is a TypeScri - `/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 +- `/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. From b4e28e1c27a9b2a31e208d8e845b2142d2e63e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 10:58:42 +0100 Subject: [PATCH 037/188] fix(catalog): make search FK migration safe for large databases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The search FK migration previously ran all DDL and a bulk DELETE in a single transaction, holding AccessExclusiveLock for the entire duration. On large tables this blocks all reads for potentially minutes or hours. This restructures the migration per database engine: - PostgreSQL: batch-deletes orphans before DDL, uses NOT VALID to skip full table scan under AccessExclusiveLock, then VALIDATE CONSTRAINT under the weaker ShareUpdateExclusiveLock - MySQL: batch-deletes orphans with LIMIT before DDL - SQLite: unchanged simple approach (no locking concerns) Also sets transaction: false so the batched deletes run outside the DDL transaction. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- ...20260214000000_search_fk_final_entities.js | 187 +++++++++++++++--- 1 file changed, 155 insertions(+), 32 deletions(-) diff --git a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js index ed26716ca9..917a4e36b2 100644 --- a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -21,49 +21,172 @@ * to final_entities(entity_id). This allows search entries to reference * final entities directly, with CASCADE delete when entities are removed. * + * For PostgreSQL and MySQL, the migration is structured to minimize lock + * time on large tables by batch-deleting orphaned rows before any DDL. + * PostgreSQL additionally uses NOT VALID / VALIDATE CONSTRAINT to keep + * the AccessExclusiveLock duration minimal. + * * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { - // Step 1: Drop the old foreign key constraint (to refresh_state) - await knex.schema.alterTable('search', table => { - table.dropForeign(['entity_id']); - }); + const client = knex.client.config.client; - // Step 2: Delete orphaned rows where entity_id doesn't exist in final_entities - await knex('search') - .whereNotIn('entity_id', knex('final_entities').select('entity_id')) - .delete(); + if (client.includes('pg')) { + // Batch-delete orphaned rows BEFORE touching the schema. + // This runs outside any DDL lock, so it doesn't block reads. + for (;;) { + const deleted = await knex.raw(` + DELETE FROM "search" + WHERE ctid IN ( + SELECT s.ctid FROM "search" s + LEFT JOIN "final_entities" fe ON s."entity_id" = fe."entity_id" + WHERE fe."entity_id" IS NULL + LIMIT 10000 + ) + `); + if (deleted.rowCount === 0) { + break; + } + } - // Step 3: Add new FK to final_entities(entity_id) with CASCADE - await knex.schema.alterTable('search', table => { - table - .foreign('entity_id') - .references('entity_id') - .inTable('final_entities') - .onDelete('CASCADE'); - }); + // Drop old FK and add new one with NOT VALID (minimal lock time). + // NOT VALID skips the full table scan — we already cleaned up orphans above. + await knex.raw( + `ALTER TABLE "search" DROP CONSTRAINT "search_entity_id_foreign"`, + ); + await knex.raw(` + ALTER TABLE "search" + ADD CONSTRAINT "search_entity_id_foreign" + FOREIGN KEY ("entity_id") REFERENCES "final_entities"("entity_id") + ON DELETE CASCADE + NOT VALID + `); + + // Validate the FK separately. This only takes a + // ShareUpdateExclusiveLock, which does NOT block reads/writes. + 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. + for (;;) { + const [result] = await knex.raw( + `DELETE FROM \`search\` WHERE \`entity_id\` NOT IN (SELECT \`entity_id\` FROM \`final_entities\`) LIMIT 10000`, + ); + if (result.affectedRows === 0) { + break; + } + } + + // Drop old FK and add new one. MySQL does not support NOT VALID, but + // the table is already clean so validation is fast. + await knex.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); + await knex.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('final_entities') + .onDelete('CASCADE'); + }); + } else { + // SQLite: simple approach, locking is not a concern + await knex.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); + + await knex('search') + .whereNotIn('entity_id', knex('final_entities').select('entity_id')) + .delete(); + + await knex.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) { - // Step 1: Drop FK to final_entities - await knex.schema.alterTable('search', table => { - table.dropForeign(['entity_id']); - }); + const client = knex.client.config.client; - // Step 2: Delete orphaned rows where entity_id doesn't exist in refresh_state - await knex('search') - .whereNotIn('entity_id', knex('refresh_state').select('entity_id')) - .delete(); + if (client.includes('pg')) { + for (;;) { + const deleted = await knex.raw(` + DELETE FROM "search" + WHERE ctid IN ( + SELECT s.ctid FROM "search" s + LEFT JOIN "refresh_state" rs ON s."entity_id" = rs."entity_id" + WHERE rs."entity_id" IS NULL + LIMIT 10000 + ) + `); + if (deleted.rowCount === 0) { + break; + } + } - // Step 3: Add back FK to refresh_state(entity_id) with CASCADE - await knex.schema.alterTable('search', table => { - table - .foreign('entity_id') - .references('entity_id') - .inTable('refresh_state') - .onDelete('CASCADE'); - }); + await knex.raw( + `ALTER TABLE "search" DROP CONSTRAINT "search_entity_id_foreign"`, + ); + await knex.raw(` + ALTER TABLE "search" + ADD CONSTRAINT "search_entity_id_foreign" + FOREIGN KEY ("entity_id") REFERENCES "refresh_state"("entity_id") + ON DELETE CASCADE + NOT VALID + `); + + await knex.raw( + `ALTER TABLE "search" VALIDATE CONSTRAINT "search_entity_id_foreign"`, + ); + } else if (client.includes('mysql')) { + for (;;) { + const [result] = await knex.raw( + `DELETE FROM \`search\` WHERE \`entity_id\` NOT IN (SELECT \`entity_id\` FROM \`refresh_state\`) LIMIT 10000`, + ); + if (result.affectedRows === 0) { + break; + } + } + + await knex.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); + await knex.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('refresh_state') + .onDelete('CASCADE'); + }); + } else { + await knex.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); + + await knex('search') + .whereNotIn('entity_id', knex('refresh_state').select('entity_id')) + .delete(); + + await knex.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, }; From ffe5febfd625e1f31fe7c4b1bc5da0c808f12356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 11:04:52 +0100 Subject: [PATCH 038/188] fix: use LEFT JOIN for MySQL batch deletes in search FK migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback: replace NOT IN subquery with LEFT JOIN for MySQL batch deletes. Since MySQL doesn't support LIMIT in multi-table DELETE, orphan entity_ids are found via SELECT first, then deleted in a separate statement. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- ...20260214000000_search_fk_final_entities.js | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js index 917a4e36b2..1b483268b0 100644 --- a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -69,13 +69,21 @@ exports.up = async function up(knex) { ); } else if (client.includes('mysql')) { // Batch-delete orphaned rows before DDL to reduce lock time. + // Uses LEFT JOIN to find orphans efficiently, then deletes by entity_id. + // MySQL doesn't support LIMIT in multi-table DELETE, so we find the + // orphaned entity_ids first, then delete in a separate statement. for (;;) { - const [result] = await knex.raw( - `DELETE FROM \`search\` WHERE \`entity_id\` NOT IN (SELECT \`entity_id\` FROM \`final_entities\`) LIMIT 10000`, - ); - if (result.affectedRows === 0) { + const [orphanIds] = await knex.raw(` + SELECT DISTINCT s.\`entity_id\` FROM \`search\` s + LEFT JOIN \`final_entities\` fe ON s.\`entity_id\` = fe.\`entity_id\` + WHERE fe.\`entity_id\` IS NULL + LIMIT 10000 + `); + if (orphanIds.length === 0) { break; } + const ids = orphanIds.map(r => r.entity_id); + await knex('search').whereIn('entity_id', ids).delete(); } // Drop old FK and add new one. MySQL does not support NOT VALID, but @@ -148,12 +156,17 @@ exports.down = async function down(knex) { ); } else if (client.includes('mysql')) { for (;;) { - const [result] = await knex.raw( - `DELETE FROM \`search\` WHERE \`entity_id\` NOT IN (SELECT \`entity_id\` FROM \`refresh_state\`) LIMIT 10000`, - ); - if (result.affectedRows === 0) { + const [orphanIds] = await knex.raw(` + SELECT DISTINCT s.\`entity_id\` FROM \`search\` s + LEFT JOIN \`refresh_state\` rs ON s.\`entity_id\` = rs.\`entity_id\` + WHERE rs.\`entity_id\` IS NULL + LIMIT 10000 + `); + if (orphanIds.length === 0) { break; } + const ids = orphanIds.map(r => r.entity_id); + await knex('search').whereIn('entity_id', ids).delete(); } await knex.schema.alterTable('search', table => { From 42a42d56251bd95e6ca030754ebdb1b5cb01d0b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 11:06:57 +0100 Subject: [PATCH 039/188] fix: add JSDoc type annotation for MySQL orphan ID mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- .../migrations/20260214000000_search_fk_final_entities.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js index 1b483268b0..9c90567dfc 100644 --- a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -82,7 +82,9 @@ exports.up = async function up(knex) { if (orphanIds.length === 0) { break; } - const ids = orphanIds.map(r => r.entity_id); + const ids = orphanIds.map( + (/** @type {{ entity_id: string }} */ r) => r.entity_id, + ); await knex('search').whereIn('entity_id', ids).delete(); } @@ -165,7 +167,9 @@ exports.down = async function down(knex) { if (orphanIds.length === 0) { break; } - const ids = orphanIds.map(r => r.entity_id); + const ids = orphanIds.map( + (/** @type {{ entity_id: string }} */ r) => r.entity_id, + ); await knex('search').whereIn('entity_id', ids).delete(); } From 65364716da916c9724523d1eb5b87aa13277d178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 11:53:28 +0100 Subject: [PATCH 040/188] fix: add NULL safety and idempotent DROP CONSTRAINT to search FK migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent orphan cleanup queries from incorrectly matching rows with NULL entity_id via LEFT JOIN, and use DROP CONSTRAINT IF EXISTS for safer partial re-runs given transaction: false. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- .../migrations/20260214000000_search_fk_final_entities.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js index 9c90567dfc..6c2136de4d 100644 --- a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -41,6 +41,7 @@ exports.up = async function up(knex) { SELECT s.ctid FROM "search" s LEFT JOIN "final_entities" fe ON s."entity_id" = fe."entity_id" WHERE fe."entity_id" IS NULL + AND s."entity_id" IS NOT NULL LIMIT 10000 ) `); @@ -52,7 +53,7 @@ exports.up = async function up(knex) { // Drop old FK and add new one with NOT VALID (minimal lock time). // NOT VALID skips the full table scan — we already cleaned up orphans above. await knex.raw( - `ALTER TABLE "search" DROP CONSTRAINT "search_entity_id_foreign"`, + `ALTER TABLE "search" DROP CONSTRAINT IF EXISTS "search_entity_id_foreign"`, ); await knex.raw(` ALTER TABLE "search" @@ -77,6 +78,7 @@ exports.up = async function up(knex) { SELECT DISTINCT s.\`entity_id\` FROM \`search\` s LEFT JOIN \`final_entities\` fe ON s.\`entity_id\` = fe.\`entity_id\` WHERE fe.\`entity_id\` IS NULL + AND s.\`entity_id\` IS NOT NULL LIMIT 10000 `); if (orphanIds.length === 0) { @@ -134,6 +136,7 @@ exports.down = async function down(knex) { SELECT s.ctid FROM "search" s LEFT JOIN "refresh_state" rs ON s."entity_id" = rs."entity_id" WHERE rs."entity_id" IS NULL + AND s."entity_id" IS NOT NULL LIMIT 10000 ) `); @@ -143,7 +146,7 @@ exports.down = async function down(knex) { } await knex.raw( - `ALTER TABLE "search" DROP CONSTRAINT "search_entity_id_foreign"`, + `ALTER TABLE "search" DROP CONSTRAINT IF EXISTS "search_entity_id_foreign"`, ); await knex.raw(` ALTER TABLE "search" @@ -162,6 +165,7 @@ exports.down = async function down(knex) { SELECT DISTINCT s.\`entity_id\` FROM \`search\` s LEFT JOIN \`refresh_state\` rs ON s.\`entity_id\` = rs.\`entity_id\` WHERE rs.\`entity_id\` IS NULL + AND s.\`entity_id\` IS NOT NULL LIMIT 10000 `); if (orphanIds.length === 0) { From 2bc7595dfa6bf4d1037f3d217f6842f8c40ceadd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:41:43 +0000 Subject: [PATCH 041/188] chore(deps): update dependency snyk-nodejs-lockfile-parser to v2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/yarn-plugin/package.json | 2 +- yarn.lock | 286 +++--------------------------- 2 files changed, 27 insertions(+), 261 deletions(-) diff --git a/packages/yarn-plugin/package.json b/packages/yarn-plugin/package.json index 0326e3d4e9..b54b422312 100644 --- a/packages/yarn-plugin/package.json +++ b/packages/yarn-plugin/package.json @@ -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/yarn.lock b/yarn.lock index 35dd067cc4..4c4758bd03 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: @@ -18958,9 +18958,9 @@ __metadata: 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" @@ -18978,10 +18978,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 @@ -21783,13 +21783,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" @@ -23670,45 +23663,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" @@ -23752,16 +23706,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" @@ -23771,16 +23715,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" @@ -23793,16 +23727,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" @@ -23834,16 +23758,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" @@ -24254,17 +24168,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" @@ -24275,24 +24178,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" @@ -24777,7 +24662,7 @@ __metadata: 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 @@ -25121,7 +25006,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 @@ -25886,13 +25771,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" @@ -27020,13 +26898,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" @@ -28100,17 +27971,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" @@ -29779,15 +29639,6 @@ __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.18.0, enhanced-resolve@npm:^5.20.0": version: 5.20.0 resolution: "enhanced-resolve@npm:5.20.0" @@ -33426,7 +33277,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: @@ -34708,7 +34559,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 @@ -35353,13 +35204,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" @@ -36435,19 +36279,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" @@ -40281,16 +40112,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" @@ -40676,7 +40497,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: @@ -40813,15 +40634,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" @@ -41395,10 +41207,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 @@ -42297,13 +42109,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" @@ -42943,7 +42748,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 @@ -43084,15 +42889,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" @@ -43140,25 +42936,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" @@ -45872,7 +45649,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 @@ -46684,14 +46461,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" @@ -46708,7 +46485,7 @@ __metadata: uuid: "npm:^8.3.0" bin: parse-nodejs-lockfile: bin/index.js - checksum: 10/2186abf1a7930ff12c5a3929c514300a0ecb47f576f64b84b265292c106aba9ec13a98cdae0b2f01449e53311d361a67cbb13ed631e718d28faaafa97971adc5 + checksum: 10/6573f7379f0005f632678b8518fd5559f91548fc3fb5f2004a92defb44e99acc46097382c7017f6f901df89733e521486229b5fcf9e6d1715b508ca1f9d46fff languageName: node linkType: hard @@ -47244,7 +47021,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: @@ -47253,17 +47030,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" @@ -48926,7 +48692,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 @@ -51216,7 +50982,7 @@ __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 From 3644b725f88912cf7045436cfa7142b001d9f6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 13:58:44 +0100 Subject: [PATCH 042/188] add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/ready-ghosts-fail.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ready-ghosts-fail.md diff --git a/.changeset/ready-ghosts-fail.md b/.changeset/ready-ghosts-fail.md new file mode 100644 index 0000000000..de3c600bc0 --- /dev/null +++ b/.changeset/ready-ghosts-fail.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Optimize a catalog migration for big databases From 1ecf82a5c29baccae68adeda04994b0e8864ab6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 14:00:52 +0100 Subject: [PATCH 043/188] catalog-backend: wrap MySQL/SQLite migration branches in explicit transactions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The search FK migration uses transaction: false for PostgreSQL's benefit, but this left MySQL and SQLite branches non-atomic. A failure between dropForeign and the new addForeign would leave the table with no FK constraint. Wrap those branches in explicit knex.transaction() calls. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- ...20260214000000_search_fk_final_entities.js | 101 ++++++++++-------- 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js index 6c2136de4d..4f3885aa17 100644 --- a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -90,34 +90,41 @@ exports.up = async function up(knex) { await knex('search').whereIn('entity_id', ids).delete(); } - // Drop old FK and add new one. MySQL does not support NOT VALID, but - // the table is already clean so validation is fast. - await knex.schema.alterTable('search', table => { - table.dropForeign(['entity_id']); - }); - await knex.schema.alterTable('search', table => { - table - .foreign('entity_id') - .references('entity_id') - .inTable('final_entities') - .onDelete('CASCADE'); + // Drop old FK and add new one inside an explicit transaction, since the + // global transaction wrapper is disabled for this migration. MySQL does + // not support NOT VALID, but the table is already clean so validation + // is fast. + await knex.transaction(async trx => { + await trx.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); + await trx.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('final_entities') + .onDelete('CASCADE'); + }); }); } else { - // SQLite: simple approach, locking is not a concern - await knex.schema.alterTable('search', table => { - table.dropForeign(['entity_id']); - }); + // 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 knex('search') - .whereNotIn('entity_id', knex('final_entities').select('entity_id')) - .delete(); + await trx('search') + .whereNotIn('entity_id', trx('final_entities').select('entity_id')) + .delete(); - await knex.schema.alterTable('search', table => { - table - .foreign('entity_id') - .references('entity_id') - .inTable('final_entities') - .onDelete('CASCADE'); + await trx.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('final_entities') + .onDelete('CASCADE'); + }); }); } }; @@ -177,31 +184,35 @@ exports.down = async function down(knex) { await knex('search').whereIn('entity_id', ids).delete(); } - await knex.schema.alterTable('search', table => { - table.dropForeign(['entity_id']); - }); - await knex.schema.alterTable('search', table => { - table - .foreign('entity_id') - .references('entity_id') - .inTable('refresh_state') - .onDelete('CASCADE'); + await knex.transaction(async trx => { + await trx.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); + await trx.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('refresh_state') + .onDelete('CASCADE'); + }); }); } else { - await knex.schema.alterTable('search', table => { - table.dropForeign(['entity_id']); - }); + await knex.transaction(async trx => { + await trx.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); - await knex('search') - .whereNotIn('entity_id', knex('refresh_state').select('entity_id')) - .delete(); + await trx('search') + .whereNotIn('entity_id', trx('refresh_state').select('entity_id')) + .delete(); - await knex.schema.alterTable('search', table => { - table - .foreign('entity_id') - .references('entity_id') - .inTable('refresh_state') - .onDelete('CASCADE'); + await trx.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('refresh_state') + .onDelete('CASCADE'); + }); }); } }; From 28f5462bd8af5b0cc88cc0eb1d54a6c529699622 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:05:08 +0000 Subject: [PATCH 044/188] chore(deps): update dependency sort-package-json to v3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 56 ++++++++++++++++++++++------------------------------ 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 4b217e8af9..fecd26d7e7 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,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/yarn.lock b/yarn.lock index 35dd067cc4..0e47b21955 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29039,10 +29039,10 @@ __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 @@ -29060,7 +29060,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 @@ -32546,13 +32546,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" @@ -32643,10 +32636,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 @@ -45681,7 +45674,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" @@ -46776,28 +46769,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 From 89a1c313d0b65e295a2b803e74cb1d800a3f29c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 14:16:25 +0100 Subject: [PATCH 045/188] catalog-backend: address review comments on search FK migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Close PG race window: drop old FK and add NOT VALID FK before batch cleanup, so no new orphans can be inserted during cleanup - Extract batch-delete helpers (batchDeleteOrphansPg, batchDeleteOrphansMysql) to reduce duplication across up/down and dialects - Fix ShareUpdateExclusiveLock comment to be more precise - Make changeset message more descriptive Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- .changeset/ready-ghosts-fail.md | 2 +- ...20260214000000_search_fk_final_entities.js | 135 ++++++++---------- 2 files changed, 64 insertions(+), 73 deletions(-) diff --git a/.changeset/ready-ghosts-fail.md b/.changeset/ready-ghosts-fail.md index de3c600bc0..a1e4a68602 100644 --- a/.changeset/ready-ghosts-fail.md +++ b/.changeset/ready-ghosts-fail.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend': patch --- -Optimize a catalog migration for big databases +Make the `search` foreign key catalog migration non-blocking on large tables by using batch deletes and PostgreSQL `NOT VALID`/`VALIDATE` to reduce lock duration diff --git a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js index 4f3885aa17..7048f2da75 100644 --- a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -16,6 +16,56 @@ // @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 @@ -32,26 +82,9 @@ exports.up = async function up(knex) { const client = knex.client.config.client; if (client.includes('pg')) { - // Batch-delete orphaned rows BEFORE touching the schema. - // This runs outside any DDL lock, so it doesn't block reads. - for (;;) { - const deleted = await knex.raw(` - DELETE FROM "search" - WHERE ctid IN ( - SELECT s.ctid FROM "search" s - LEFT JOIN "final_entities" fe ON s."entity_id" = fe."entity_id" - WHERE fe."entity_id" IS NULL - AND s."entity_id" IS NOT NULL - LIMIT 10000 - ) - `); - if (deleted.rowCount === 0) { - break; - } - } - - // Drop old FK and add new one with NOT VALID (minimal lock time). - // NOT VALID skips the full table scan — we already cleaned up orphans above. + // Drop old FK and immediately add the new one as NOT VALID. This + // prevents new orphan rows from being inserted while we clean up + // existing ones, closing the race window between cleanup and FK add. await knex.raw( `ALTER TABLE "search" DROP CONSTRAINT IF EXISTS "search_entity_id_foreign"`, ); @@ -63,32 +96,19 @@ exports.up = async function up(knex) { 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 reads/writes. + // 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. - // Uses LEFT JOIN to find orphans efficiently, then deletes by entity_id. - // MySQL doesn't support LIMIT in multi-table DELETE, so we find the - // orphaned entity_ids first, then delete in a separate statement. - for (;;) { - const [orphanIds] = await knex.raw(` - SELECT DISTINCT s.\`entity_id\` FROM \`search\` s - LEFT JOIN \`final_entities\` fe ON s.\`entity_id\` = fe.\`entity_id\` - WHERE fe.\`entity_id\` IS NULL - AND s.\`entity_id\` IS NOT NULL - LIMIT 10000 - `); - 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(); - } + await batchDeleteOrphansMysql(knex, 'final_entities'); // Drop old FK and add new one inside an explicit transaction, since the // global transaction wrapper is disabled for this migration. MySQL does @@ -136,22 +156,6 @@ exports.down = async function down(knex) { const client = knex.client.config.client; if (client.includes('pg')) { - for (;;) { - const deleted = await knex.raw(` - DELETE FROM "search" - WHERE ctid IN ( - SELECT s.ctid FROM "search" s - LEFT JOIN "refresh_state" rs ON s."entity_id" = rs."entity_id" - WHERE rs."entity_id" IS NULL - AND s."entity_id" IS NOT NULL - LIMIT 10000 - ) - `); - if (deleted.rowCount === 0) { - break; - } - } - await knex.raw( `ALTER TABLE "search" DROP CONSTRAINT IF EXISTS "search_entity_id_foreign"`, ); @@ -163,26 +167,13 @@ exports.down = async function down(knex) { NOT VALID `); + await batchDeleteOrphansPg(knex, 'refresh_state'); + await knex.raw( `ALTER TABLE "search" VALIDATE CONSTRAINT "search_entity_id_foreign"`, ); } else if (client.includes('mysql')) { - for (;;) { - const [orphanIds] = await knex.raw(` - SELECT DISTINCT s.\`entity_id\` FROM \`search\` s - LEFT JOIN \`refresh_state\` rs ON s.\`entity_id\` = rs.\`entity_id\` - WHERE rs.\`entity_id\` IS NULL - AND s.\`entity_id\` IS NOT NULL - LIMIT 10000 - `); - 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(); - } + await batchDeleteOrphansMysql(knex, 'refresh_state'); await knex.transaction(async trx => { await trx.schema.alterTable('search', table => { From 09c7e49f82e8d7a9d32ec8009db4fdcf63a4e6e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 14:43:10 +0100 Subject: [PATCH 046/188] catalog-backend: combine PG DROP+ADD into single ALTER TABLE, fix MySQL comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Combine DROP CONSTRAINT and ADD CONSTRAINT into a single ALTER TABLE statement for PostgreSQL, eliminating the brief window where no FK exists - Reword MySQL transaction comment to clarify that ALTER TABLE causes implicit commits in InnoDB, so the wrapper doesn't provide full atomicity Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- ...20260214000000_search_fk_final_entities.js | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js index 7048f2da75..71caeb600d 100644 --- a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -82,14 +82,13 @@ 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. This - // prevents new orphan rows from being inserted while we clean up - // existing ones, closing the race window between cleanup and FK add. - await knex.raw( - `ALTER TABLE "search" DROP CONSTRAINT IF EXISTS "search_entity_id_foreign"`, - ); + // 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 @@ -110,10 +109,10 @@ exports.up = async function up(knex) { // Batch-delete orphaned rows before DDL to reduce lock time. await batchDeleteOrphansMysql(knex, 'final_entities'); - // Drop old FK and add new one inside an explicit transaction, since the - // global transaction wrapper is disabled for this migration. MySQL does - // not support NOT VALID, but the table is already clean so validation - // is fast. + // Perform the FK changes. Note that in MySQL/InnoDB, ALTER TABLE + // statements cause implicit commits, so wrapping in a transaction does + // not provide full atomicity. However the table is already cleaned of + // orphans and validation remains fast. await knex.transaction(async trx => { await trx.schema.alterTable('search', table => { table.dropForeign(['entity_id']); @@ -156,11 +155,9 @@ 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"`, - ); 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 From 1b69ff9c82f1553cdabcca458173060dd21a2224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 14:56:45 +0100 Subject: [PATCH 047/188] catalog-backend: make MySQL FK swap idempotent with retry loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the non-atomic MySQL transaction wrapper with a retry loop that checks information_schema before dropping the FK and retries the add if new orphan rows appear during the window between DROP and ADD. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- ...20260214000000_search_fk_final_entities.js | 93 +++++++++++++------ 1 file changed, 65 insertions(+), 28 deletions(-) diff --git a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js index 71caeb600d..4e1fc53d24 100644 --- a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -109,22 +109,41 @@ exports.up = async function up(knex) { // Batch-delete orphaned rows before DDL to reduce lock time. await batchDeleteOrphansMysql(knex, 'final_entities'); - // Perform the FK changes. Note that in MySQL/InnoDB, ALTER TABLE - // statements cause implicit commits, so wrapping in a transaction does - // not provide full atomicity. However the table is already cleaned of - // orphans and validation remains fast. - await knex.transaction(async trx => { - await trx.schema.alterTable('search', table => { - table.dropForeign(['entity_id']); - }); - await trx.schema.alterTable('search', table => { - table - .foreign('entity_id') - .references('entity_id') - .inTable('final_entities') - .onDelete('CASCADE'); - }); - }); + // 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. @@ -172,18 +191,36 @@ exports.down = async function down(knex) { } else if (client.includes('mysql')) { await batchDeleteOrphansMysql(knex, 'refresh_state'); - await knex.transaction(async trx => { - await trx.schema.alterTable('search', table => { - table.dropForeign(['entity_id']); - }); - await trx.schema.alterTable('search', table => { - table - .foreign('entity_id') - .references('entity_id') - .inTable('refresh_state') - .onDelete('CASCADE'); - }); - }); + 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 => { From 34af7b45510c30431d989521c01502f117d91a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 15:03:06 +0100 Subject: [PATCH 048/188] catalog-backend: improve search FK migration test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests for NULL entity_id rows surviving migration, FK enforcement rejecting invalid inserts, and down migration orphan cleanup. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- .../src/tests/migrations.test.ts | 203 ++++++++++-------- 1 file changed, 114 insertions(+), 89 deletions(-) diff --git a/plugins/catalog-backend/src/tests/migrations.test.ts b/plugins/catalog-backend/src/tests/migrations.test.ts index 4a469ab07e..24a23b2888 100644 --- a/plugins/catalog-backend/src/tests/migrations.test.ts +++ b/plugins/catalog-backend/src/tests/migrations.test.ts @@ -784,67 +784,96 @@ describe('migrations', () => { 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(6); + expect(Number(preMigrationCount[0].count)).toBe(7); // Run the migration await migrateUpOnce(knex); - // Verify orphaned rows (id3) were deleted since id3 is not in final_entities - const postMigrationRows = await knex('search').orderBy([ - 'entity_id', - 'key', - ]); - expect(postMigrationRows).toEqual([ - { - entity_id: 'id1', + // 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: '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', - }, - ]); + 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').orderBy([ - 'entity_id', - 'key', - ]); - expect(searchAfterDelete).toEqual([ - { - entity_id: 'id2', - key: 'kind', - value: 'component', - original_value: 'Component', - }, - { - entity_id: 'id2', - key: 'metadata.name', - value: 'service-b', - original_value: 'service-b', - }, - ]); + 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({ @@ -863,53 +892,49 @@ describe('migrations', () => { }, ]); + // 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 data is still intact after down migration - const revertedSearchRows = await knex('search').orderBy([ - 'entity_id', - 'key', - ]); - expect(revertedSearchRows).toEqual([ - { - entity_id: 'id1', - key: 'kind', - value: 'component', - original_value: 'Component', - }, - { - entity_id: 'id2', - key: 'kind', - value: 'component', - original_value: 'Component', - }, - { - entity_id: 'id2', - key: 'metadata.name', - value: 'service-b', - original_value: 'service-b', - }, - ]); + // 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: 'id1' }).delete(); - const afterRefreshStateDelete = await knex('search').orderBy([ - 'entity_id', - 'key', - ]); + await knex('refresh_state').where({ entity_id: 'id2' }).delete(); + const afterRefreshStateDelete = await knex('search'); expect(afterRefreshStateDelete).toEqual([ { - entity_id: 'id2', - key: 'kind', - value: 'component', - original_value: 'Component', - }, - { - entity_id: 'id2', - key: 'metadata.name', - value: 'service-b', - original_value: 'service-b', + entity_id: null, + key: 'global', + value: 'setting', + original_value: 'setting', }, ]); From 2b87d22ad415f26794a68022199bcd7dd5af1569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 16:19:19 +0100 Subject: [PATCH 049/188] Update plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Fredrik Adelöw --- .../20260214000000_search_fk_final_entities.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js index 4e1fc53d24..4d43baf9a8 100644 --- a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -71,10 +71,14 @@ async function batchDeleteOrphansMysql(knex, refTable) { * to final_entities(entity_id). This allows search entries to reference * final entities directly, with CASCADE delete when entities are removed. * - * For PostgreSQL and MySQL, the migration is structured to minimize lock - * time on large tables by batch-deleting orphaned rows before any DDL. - * PostgreSQL additionally uses NOT VALID / VALIDATE CONSTRAINT to keep - * the AccessExclusiveLock duration minimal. + * 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 */ From 85f38c29d19c1f0673853fce41ba0158f947dcc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 16:47:24 +0100 Subject: [PATCH 050/188] devtools: Add cancel task operation to scheduled tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames useTriggerScheduledTask to useScheduledTasksOperations and adds cancelTask alongside triggerTask, with shared isLoading/error state. Adds CancelScheduledTask type to devtools-common and cancelScheduledTask to the DevToolsApi interface and client, hitting the existing scheduler cancel endpoint. The ScheduledTasksContent UI now shows both trigger and cancel buttons per task row. Signed-off-by: Fredrik Adelöw Made-with: Cursor --- plugins/devtools-common/src/alpha.ts | 1 + plugins/devtools-common/src/types.ts | 5 ++ plugins/devtools/src/api/DevToolsApi.ts | 5 ++ plugins/devtools/src/api/DevToolsClient.ts | 22 ++++++ .../ScheduledTasksContent.tsx | 73 +++++++++++++------ plugins/devtools/src/hooks/index.ts | 2 +- ...Task.ts => useScheduledTasksOperations.ts} | 31 ++++++-- 7 files changed, 108 insertions(+), 31 deletions(-) rename plugins/devtools/src/hooks/{useTriggerScheduledTask.ts => useScheduledTasksOperations.ts} (66%) diff --git a/plugins/devtools-common/src/alpha.ts b/plugins/devtools-common/src/alpha.ts index 984967c4f3..d7f9f94d76 100644 --- a/plugins/devtools-common/src/alpha.ts +++ b/plugins/devtools-common/src/alpha.ts @@ -18,6 +18,7 @@ export { devToolsTaskSchedulerCreatePermission, } from './permissions'; export type { + CancelScheduledTask, ScheduledTasks, TaskApiTasksResponse, TriggerScheduledTask, diff --git a/plugins/devtools-common/src/types.ts b/plugins/devtools-common/src/types.ts index dc35188fde..3879fb1ae3 100644 --- a/plugins/devtools-common/src/types.ts +++ b/plugins/devtools-common/src/types.ts @@ -133,3 +133,8 @@ export type ScheduledTasks = { export type TriggerScheduledTask = { error?: string; }; + +/** @alpha */ +export type CancelScheduledTask = { + error?: string; +}; diff --git a/plugins/devtools/src/api/DevToolsApi.ts b/plugins/devtools/src/api/DevToolsApi.ts index 4fc579ee06..6894a0a8b2 100644 --- a/plugins/devtools/src/api/DevToolsApi.ts +++ b/plugins/devtools/src/api/DevToolsApi.ts @@ -21,6 +21,7 @@ import { ExternalDependency, } from '@backstage/plugin-devtools-common'; import { + CancelScheduledTask, ScheduledTasks, TriggerScheduledTask, } from '@backstage/plugin-devtools-common/alpha'; @@ -38,4 +39,8 @@ export interface DevToolsApi { plugin: string, taskId: string, ): Promise; + cancelScheduledTask( + plugin: string, + taskId: string, + ): Promise; } diff --git a/plugins/devtools/src/api/DevToolsClient.ts b/plugins/devtools/src/api/DevToolsClient.ts index 0577757838..c5b50193ea 100644 --- a/plugins/devtools/src/api/DevToolsClient.ts +++ b/plugins/devtools/src/api/DevToolsClient.ts @@ -21,6 +21,7 @@ import { ExternalDependency, } from '@backstage/plugin-devtools-common'; import { + CancelScheduledTask, ScheduledTasks, TriggerScheduledTask, } from '@backstage/plugin-devtools-common/alpha'; @@ -85,6 +86,27 @@ export class DevToolsClient implements DevToolsApi { return response.json() as Promise; } + 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) { + throw await ResponseError.fromResponse(response); + } + + return response.json() as Promise; + } + public async getExternalDependencies(): Promise< ExternalDependency[] | undefined > { diff --git a/plugins/devtools/src/components/Content/ScheduledTasksContent/ScheduledTasksContent.tsx b/plugins/devtools/src/components/Content/ScheduledTasksContent/ScheduledTasksContent.tsx index 7517cf8ea5..d115968a77 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, 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, }; }; From f80195ec3f18b137f2cf1e9a3dd0b531eab3f68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 16:51:25 +0100 Subject: [PATCH 051/188] Add changeset for devtools cancel task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw Made-with: Cursor Signed-off-by: Fredrik Adelöw Made-with: Cursor --- .changeset/devtools-cancel-scheduled-task.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/devtools-cancel-scheduled-task.md diff --git a/.changeset/devtools-cancel-scheduled-task.md b/.changeset/devtools-cancel-scheduled-task.md new file mode 100644 index 0000000000..ac057d61b6 --- /dev/null +++ b/.changeset/devtools-cancel-scheduled-task.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-devtools-common': patch +'@backstage/plugin-devtools': patch +--- + +Added `cancelScheduledTask` to the DevTools API and a cancel button to the scheduled tasks UI. The `useTriggerScheduledTask` hook has been renamed to `useScheduledTasksOperations`, now exposing both `triggerTask` and `cancelTask` with shared loading and error state. From 6b6b5de5148c21de14d1ca453a2ef8dd831a0082 Mon Sep 17 00:00:00 2001 From: Rickard Dybeck Date: Wed, 11 Mar 2026 12:09:52 -0400 Subject: [PATCH 052/188] feat(kubernetes): add DNS endpoint support to GKE cluster locator (#33297) * feat(kubernetes): add DNS endpoint support to GKE cluster locator Add endpointType config option to GkeClusterLocator allowing use of DNS-based control plane endpoints instead of public IP endpoints. When set to 'dns', the locator uses the cluster's DNS endpoint (e.g. gke-..gke.goog) with fallback to public IP if no DNS endpoint is available. Signed-off-by: Rickard Dybeck * fix: fix relevant AI review comments Signed-off-by: Rickard Dybeck --------- Signed-off-by: Rickard Dybeck --- .changeset/gke-dns-endpoint-support.md | 5 + plugins/kubernetes-backend/config.d.ts | 7 + .../cluster-locator/GkeClusterLocator.test.ts | 239 +++++++++++++++--- .../src/cluster-locator/GkeClusterLocator.ts | 55 +++- .../src/cluster-locator/index.ts | 1 + 5 files changed, 265 insertions(+), 42 deletions(-) create mode 100644 .changeset/gke-dns-endpoint-support.md diff --git a/.changeset/gke-dns-endpoint-support.md b/.changeset/gke-dns-endpoint-support.md new file mode 100644 index 0000000000..54361c5115 --- /dev/null +++ b/.changeset/gke-dns-endpoint-support.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Added `endpointType` config option to the GKE cluster locator, allowing use of DNS-based control plane endpoints instead of public IP endpoints. Set `endpointType: 'dns'` to use GKE DNS endpoints (e.g. `gke-..gke.goog`) which provide proper TLS certificates and IAM-based access control. 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/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: From 9110c16c8a681351dd42201dc17cd49da8f3becc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 19:41:10 +0100 Subject: [PATCH 053/188] api report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/devtools-common/report-alpha.api.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/devtools-common/report-alpha.api.md b/plugins/devtools-common/report-alpha.api.md index 3c3b7ff89e..e11b2d62ca 100644 --- a/plugins/devtools-common/report-alpha.api.md +++ b/plugins/devtools-common/report-alpha.api.md @@ -6,6 +6,11 @@ import { BasicPermission } from '@backstage/plugin-permission-common'; import { JsonObject } from '@backstage/types'; +// @alpha (undocumented) +export type CancelScheduledTask = { + error?: string; +}; + // @alpha (undocumented) export const devToolsTaskSchedulerCreatePermission: BasicPermission; From 239c8dcfd2af169f6e3caecf1608a018618e2132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 20:29:45 +0100 Subject: [PATCH 054/188] cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/devtools-common/report-alpha.api.md | 5 ----- plugins/devtools-common/src/alpha.ts | 2 +- plugins/devtools-common/src/types.ts | 5 ----- plugins/devtools/src/api/DevToolsApi.ts | 6 +----- plugins/devtools/src/api/DevToolsClient.ts | 14 ++++++++------ .../ScheduledTasksContent.tsx | 5 +++-- 6 files changed, 13 insertions(+), 24 deletions(-) diff --git a/plugins/devtools-common/report-alpha.api.md b/plugins/devtools-common/report-alpha.api.md index e11b2d62ca..3c3b7ff89e 100644 --- a/plugins/devtools-common/report-alpha.api.md +++ b/plugins/devtools-common/report-alpha.api.md @@ -6,11 +6,6 @@ import { BasicPermission } from '@backstage/plugin-permission-common'; import { JsonObject } from '@backstage/types'; -// @alpha (undocumented) -export type CancelScheduledTask = { - error?: string; -}; - // @alpha (undocumented) export const devToolsTaskSchedulerCreatePermission: BasicPermission; diff --git a/plugins/devtools-common/src/alpha.ts b/plugins/devtools-common/src/alpha.ts index d7f9f94d76..7abe4b1f5e 100644 --- a/plugins/devtools-common/src/alpha.ts +++ b/plugins/devtools-common/src/alpha.ts @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { devToolsTaskSchedulerReadPermission, devToolsTaskSchedulerCreatePermission, } from './permissions'; export type { - CancelScheduledTask, ScheduledTasks, TaskApiTasksResponse, TriggerScheduledTask, diff --git a/plugins/devtools-common/src/types.ts b/plugins/devtools-common/src/types.ts index 3879fb1ae3..dc35188fde 100644 --- a/plugins/devtools-common/src/types.ts +++ b/plugins/devtools-common/src/types.ts @@ -133,8 +133,3 @@ export type ScheduledTasks = { export type TriggerScheduledTask = { error?: string; }; - -/** @alpha */ -export type CancelScheduledTask = { - error?: string; -}; diff --git a/plugins/devtools/src/api/DevToolsApi.ts b/plugins/devtools/src/api/DevToolsApi.ts index 6894a0a8b2..2aaad49aa8 100644 --- a/plugins/devtools/src/api/DevToolsApi.ts +++ b/plugins/devtools/src/api/DevToolsApi.ts @@ -21,7 +21,6 @@ import { ExternalDependency, } from '@backstage/plugin-devtools-common'; import { - CancelScheduledTask, ScheduledTasks, TriggerScheduledTask, } from '@backstage/plugin-devtools-common/alpha'; @@ -39,8 +38,5 @@ export interface DevToolsApi { plugin: string, taskId: string, ): Promise; - cancelScheduledTask( - plugin: string, - taskId: string, - ): Promise; + cancelScheduledTask(plugin: string, taskId: string): Promise; } diff --git a/plugins/devtools/src/api/DevToolsClient.ts b/plugins/devtools/src/api/DevToolsClient.ts index c5b50193ea..b6d9d4b119 100644 --- a/plugins/devtools/src/api/DevToolsClient.ts +++ b/plugins/devtools/src/api/DevToolsClient.ts @@ -21,11 +21,10 @@ import { ExternalDependency, } from '@backstage/plugin-devtools-common'; import { - CancelScheduledTask, 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 { @@ -83,13 +82,13 @@ export class DevToolsClient implements DevToolsApi { throw await ResponseError.fromResponse(response); } - return response.json() as Promise; + return {}; } public async cancelScheduledTask( plugin: string, taskId: string, - ): Promise { + ): Promise { const baseUrl = `${await this.discoveryApi.getBaseUrl(plugin)}/`; const url = new URL( `.backstage/scheduler/v1/tasks/${encodeURIComponent(taskId)}/cancel`, @@ -101,10 +100,13 @@ export class DevToolsClient implements DevToolsApi { }); 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); } - - return response.json() as Promise; } 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 d115968a77..af86b9b237 100644 --- a/plugins/devtools/src/components/Content/ScheduledTasksContent/ScheduledTasksContent.tsx +++ b/plugins/devtools/src/components/Content/ScheduledTasksContent/ScheduledTasksContent.tsx @@ -288,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 || []} From 5fbe51d71360dcf5411355d82f77a28a119861cd Mon Sep 17 00:00:00 2001 From: abdellahhanane Date: Wed, 11 Mar 2026 21:27:58 -0400 Subject: [PATCH 055/188] Add log for the workflow run url Signed-off-by: abdellahhanane --- .../src/actions/githubActionsDispatch.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts index c882869085..4cc10235a1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts @@ -167,6 +167,8 @@ export function createGithubActionsDispatchAction(options: { ctx.output('workflowRunId', runDetails.workflowRunId); ctx.output('workflowRunUrl', runDetails.workflowRunUrl); ctx.output('workflowRunHtmlUrl', runDetails.workflowRunHtmlUrl); + + ctx.logger.info(`Workflow run url: ${runDetails.workflowRunHtmlUrl}`); } } catch (e) { assertError(e); From 325feafebf6e1fa30c7d16fd20f8c8c19f845f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 12 Mar 2026 09:24:38 +0100 Subject: [PATCH 056/188] Update .changeset/devtools-cancel-scheduled-task.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/devtools-cancel-scheduled-task.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/devtools-cancel-scheduled-task.md b/.changeset/devtools-cancel-scheduled-task.md index ac057d61b6..fadd51f2aa 100644 --- a/.changeset/devtools-cancel-scheduled-task.md +++ b/.changeset/devtools-cancel-scheduled-task.md @@ -3,4 +3,4 @@ '@backstage/plugin-devtools': patch --- -Added `cancelScheduledTask` to the DevTools API and a cancel button to the scheduled tasks UI. The `useTriggerScheduledTask` hook has been renamed to `useScheduledTasksOperations`, now exposing both `triggerTask` and `cancelTask` with shared loading and error state. +Added `cancelScheduledTask` to the DevTools API and a cancel button to the scheduled tasks UI. From a91bd1b8a0b5fbc7abede7cca44bdc11475fac6f Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Wed, 11 Mar 2026 09:24:30 +0200 Subject: [PATCH 057/188] fix(catalog): make catalog entity deletion transactional additionally, make the catalog build respect the deferred stitching mode after deleting entities. Signed-off-by: Hellgren Heikki --- .../catalog-remove-entity-transaction.md | 5 + .../src/service/DefaultEntitiesCatalog.ts | 168 +++++++++--------- 2 files changed, 90 insertions(+), 83 deletions(-) create mode 100644 .changeset/catalog-remove-entity-transaction.md 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/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index 38cd145421..7fda0ac0f1 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -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 { From 4ec13e7ceaa5cc95dd7fdc4a4ede316eb4dc6a4d Mon Sep 17 00:00:00 2001 From: zeshanziya Date: Thu, 12 Mar 2026 14:59:54 +0530 Subject: [PATCH 058/188] docs: document full text filtering section of entity api Signed-off-by: zeshanziya --- docs/features/software-catalog/api.md | 49 ++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/features/software-catalog/api.md b/docs/features/software-catalog/api.md index 97f579c5a3..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 From 05c704d07f6f7599cada68ec78e52eae91f48601 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:06:23 +0000 Subject: [PATCH 059/188] chore(deps): update dependency flatted to v3.4.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 98f77f82f1..02b2a3d510 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31682,9 +31682,9 @@ __metadata: linkType: hard "flatted@npm:^3.1.0, flatted@npm:^3.2.7, flatted@npm:^3.3.4": - version: 3.4.0 - resolution: "flatted@npm:3.4.0" - checksum: 10/6007896f62acb93c811aef52d962c5cd2fa3c703eacfc4146865c3fc8964d3e466c66e10b1c175a84ce0a207fdc761e2e70d7ca982c82604257d45866169af6a + version: 3.4.1 + resolution: "flatted@npm:3.4.1" + checksum: 10/39a308e2ef82d2d8c80ebc74b67d4ff3f668be168137b649440b6735eb9c115d1e0c13ab0d9958b3d2ea9c85087ab7e14c14aa6f625a22b2916d89bbd91bc4a0 languageName: node linkType: hard From 39ba427d8ab6f603e20d185be3c27184b2cb8c9b Mon Sep 17 00:00:00 2001 From: chanchalkhatri19 Date: Thu, 12 Mar 2026 11:31:24 +0000 Subject: [PATCH 060/188] docs: fix typos and grammar errors across documentation Signed-off-by: chanchalkhatri19 --- docs/api/deprecations.md | 2 +- docs/integrations/azure-blobStorage/discovery.md | 2 +- docs/integrations/azure/discovery.md | 2 +- docs/integrations/azure/org.md | 2 +- docs/integrations/ldap/org.md | 2 +- docs/overview/threat-model.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) 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/integrations/azure-blobStorage/discovery.md b/docs/integrations/azure-blobStorage/discovery.md index f0953815c2..f0019a5c01 100644 --- a/docs/integrations/azure-blobStorage/discovery.md +++ b/docs/integrations/azure-blobStorage/discovery.md @@ -61,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 0eafc938a2..67d70c7f6d 100644 --- a/docs/integrations/azure/discovery.md +++ b/docs/integrations/azure/discovery.md @@ -97,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 92f3f9c12a..7bd93f38b3 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -41,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/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/overview/threat-model.md b/docs/overview/threat-model.md index 60f22a374c..5c33c5f8e6 100644 --- a/docs/overview/threat-model.md +++ b/docs/overview/threat-model.md @@ -30,7 +30,7 @@ This section assumes that you are using the 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. From 269b85ef0dd2c3f13d7caa24632a744bce44b3fb Mon Sep 17 00:00:00 2001 From: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:03:13 -0400 Subject: [PATCH 061/188] feat: use branch name instead of tag for stable docs (#32614) * feat: use branch name instead of tag for stable docs Signed-off-by: aramissennyeydd * test Signed-off-by: aramissennyeydd * paginate everything Signed-off-by: aramissennyeydd * different return value Signed-off-by: aramissennyeydd * update both flows Signed-off-by: aramissennyeydd * add docs Signed-off-by: aramissennyeydd * Change checkout ref from tags to heads Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> --------- Signed-off-by: aramissennyeydd Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> --- .github/workflows/deploy_microsite.yml | 21 +++++++++++---------- .github/workflows/verify_microsite.yml | 19 ++++++++++--------- microsite/README.md | 5 +++++ 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index c05a14ffe6..bc45c28cc1 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -27,36 +27,37 @@ jobs: 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@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -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/verify_microsite.yml b/.github/workflows/verify_microsite.yml index 80adffbba2..b8ce5bfb93 100644 --- a/.github/workflows/verify_microsite.yml +++ b/.github/workflows/verify_microsite.yml @@ -32,36 +32,37 @@ jobs: 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@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 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. From c3547fdec3ffc3c69a677b78dccde297e395f323 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:16:24 +0000 Subject: [PATCH 062/188] chore(deps): update dependency express-rate-limit to v8.3.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 02b2a3d510..0c110d2639 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31006,13 +31006,13 @@ __metadata: linkType: hard "express-rate-limit@npm:^8.2.1, express-rate-limit@npm:^8.2.2": - version: 8.3.0 - resolution: "express-rate-limit@npm:8.3.0" + version: 8.3.1 + resolution: "express-rate-limit@npm:8.3.1" dependencies: ip-address: "npm:10.1.0" peerDependencies: express: ">= 4.11" - checksum: 10/e896a66fecc10639e65873186fdfb71f19d6af650220eb7ea5450725215c3eed8dc6ddcfa1e68a9db8c9facc3326fbc281512ad3ccd8f107f42a2466ce12c18c + checksum: 10/dd97bfc48c01a6d4c5433203232b5e7a1e55e21322bde49033e5f8c4339584fe671a94096144a0810f4ea21dcec8aaaf15823109627e609f8ed1bc5912a345cf languageName: node linkType: hard From 9d5f3baf1ec81acad46a261525e34d97651aaeed Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 12 Mar 2026 16:11:56 +0000 Subject: [PATCH 063/188] Update styles and types on Checkbox Signed-off-by: Charles de Dreuille --- .changeset/young-squids-end.md | 7 +++++ packages/ui/report.api.md | 8 ------ .../components/Checkbox/Checkbox.module.css | 2 +- .../components/Checkbox/Checkbox.stories.tsx | 27 +++++++++++++++++++ .../ui/src/components/Checkbox/Checkbox.tsx | 2 +- .../ui/src/components/Checkbox/definition.ts | 2 -- packages/ui/src/components/Checkbox/types.ts | 2 -- 7 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 .changeset/young-squids-end.md diff --git a/.changeset/young-squids-end.md b/.changeset/young-squids-end.md new file mode 100644 index 0000000000..d5b5b97c1a --- /dev/null +++ b/.changeset/young-squids-end.md @@ -0,0 +1,7 @@ +--- +'@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. + +**Affected components:** Checkbox diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 12159d7818..834251f088 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -830,12 +830,6 @@ export const CheckboxDefinition: { readonly indicator: 'bui-CheckboxIndicator'; }; readonly propDefs: { - readonly selected: { - readonly dataAttribute: true; - }; - readonly indeterminate: { - readonly dataAttribute: true; - }; readonly children: {}; readonly className: {}; }; @@ -843,8 +837,6 @@ export const CheckboxDefinition: { // @public (undocumented) export type CheckboxOwnProps = { - selected?: boolean; - indeterminate?: boolean; children: React.ReactNode; className?: string; }; 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 a825ccfb8a..62599d7ae9 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.tsx +++ b/packages/ui/src/components/Checkbox/Checkbox.tsx @@ -46,7 +46,7 @@ export const Checkbox = forwardRef( )} - {children} + {children} )} diff --git a/packages/ui/src/components/Checkbox/definition.ts b/packages/ui/src/components/Checkbox/definition.ts index 5aa2473eac..b6666924ee 100644 --- a/packages/ui/src/components/Checkbox/definition.ts +++ b/packages/ui/src/components/Checkbox/definition.ts @@ -29,8 +29,6 @@ export const CheckboxDefinition = defineComponent()({ indicator: 'bui-CheckboxIndicator', }, propDefs: { - selected: { dataAttribute: true }, - indeterminate: { dataAttribute: true }, children: {}, className: {}, }, diff --git a/packages/ui/src/components/Checkbox/types.ts b/packages/ui/src/components/Checkbox/types.ts index 8507df1e5b..d9b220eb2a 100644 --- a/packages/ui/src/components/Checkbox/types.ts +++ b/packages/ui/src/components/Checkbox/types.ts @@ -17,8 +17,6 @@ import type { CheckboxProps as RACheckboxProps } from 'react-aria-components'; /** @public */ export type CheckboxOwnProps = { - selected?: boolean; - indeterminate?: boolean; children: React.ReactNode; className?: string; }; From 27c2c24a1ae4e37ed65931fe1f643e2be62c16cb Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 12 Mar 2026 16:27:40 +0000 Subject: [PATCH 064/188] Add migration notes Signed-off-by: Charles de Dreuille --- .changeset/young-squids-end.md | 3 +++ packages/ui/src/components/Checkbox/Checkbox.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.changeset/young-squids-end.md b/.changeset/young-squids-end.md index d5b5b97c1a..9ea2cee92b 100644 --- a/.changeset/young-squids-end.md +++ b/.changeset/young-squids-end.md @@ -4,4 +4,7 @@ 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/packages/ui/src/components/Checkbox/Checkbox.tsx b/packages/ui/src/components/Checkbox/Checkbox.tsx index 62599d7ae9..d7b0bd6f76 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.tsx +++ b/packages/ui/src/components/Checkbox/Checkbox.tsx @@ -46,7 +46,7 @@ export const Checkbox = forwardRef( )} - {children} +
{children}
)} From ad0adafaea79dc7a04f4dbe5086b53fb9ece1f1b Mon Sep 17 00:00:00 2001 From: abdellahhanane Date: Thu, 12 Mar 2026 13:41:43 -0400 Subject: [PATCH 065/188] Add check on workflowRunHtmlUrl is not undefined Signed-off-by: abdellahhanane --- .../src/actions/githubActionsDispatch.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts index 4cc10235a1..00dd72a9e3 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts @@ -168,7 +168,11 @@ export function createGithubActionsDispatchAction(options: { ctx.output('workflowRunUrl', runDetails.workflowRunUrl); ctx.output('workflowRunHtmlUrl', runDetails.workflowRunHtmlUrl); - ctx.logger.info(`Workflow run url: ${runDetails.workflowRunHtmlUrl}`); + if (runDetails.workflowRunHtmlUrl) { + ctx.logger.info( + `Workflow run url: ${runDetails.workflowRunHtmlUrl}`, + ); + } } } catch (e) { assertError(e); From 06294aafba41017034170e5df3fd8dc582db60b0 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Fri, 13 Mar 2026 09:51:53 +0100 Subject: [PATCH 066/188] feat(auth): migrate auth plugin from Material-UI to Backstage UI (#33282) Signed-off-by: benjdlambert --- .changeset/auth-migrate-to-bui.md | 5 + plugins/auth/package.json | 7 +- .../ConsentPage/ConsentPage.module.css | 44 +++ .../components/ConsentPage/ConsentPage.tsx | 271 +++++++----------- yarn.lock | 7 +- 5 files changed, 164 insertions(+), 170 deletions(-) create mode 100644 .changeset/auth-migrate-to-bui.md create mode 100644 plugins/auth/src/components/ConsentPage/ConsentPage.module.css 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/plugins/auth/package.json b/plugins/auth/package.json index 5b51833436..52a51d81ce 100644 --- a/plugins/auth/package.json +++ b/plugins/auth/package.json @@ -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/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/yarn.lock b/yarn.lock index 0c110d2639..fbc65dcfdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4394,16 +4394,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" From aaafc3def28cdb6f16945672e6d3b342bc986aa5 Mon Sep 17 00:00:00 2001 From: chanchalkhatri Date: Fri, 13 Mar 2026 18:44:50 +0530 Subject: [PATCH 067/188] docs: fix broken link in git catalog module README. (#33336) Signed-off-by: chanchalkhatri19 --- plugins/catalog-backend-module-gitea/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 7f84d5316150c3bfc3087e1d1b1bd5a82fb21aaf Mon Sep 17 00:00:00 2001 From: chanchalkhatri Date: Fri, 13 Mar 2026 18:59:20 +0530 Subject: [PATCH 068/188] docs: fix malformed HTML href in documentation landing page (#33338) Signed-off-by: chanchalkhatri19 --- docs/landing-page/doc-landing-page.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/landing-page/doc-landing-page.md b/docs/landing-page/doc-landing-page.md index 98a0ad9fb9..b7cc44c696 100644 --- a/docs/landing-page/doc-landing-page.md +++ b/docs/landing-page/doc-landing-page.md @@ -115,7 +115,7 @@ description: Documentation landing page. From 26c09a1883b00a43089009322aacbd2e0fcf3492 Mon Sep 17 00:00:00 2001 From: chanchalkhatri19 Date: Fri, 13 Mar 2026 11:33:44 +0000 Subject: [PATCH 069/188] docs: add Okta entity provider documentation Signed-off-by: chanchalkhatri19 --- docs/integrations/okta/org.md | 23 +++++++++++++++++++++++ microsite/sidebars.ts | 1 + 2 files changed, 24 insertions(+) create mode 100644 docs/integrations/okta/org.md 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/microsite/sidebars.ts b/microsite/sidebars.ts index 7e4e41591b..bfb030a6f8 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -399,6 +399,7 @@ export default { 'integrations/google-cloud-storage/locations', ]), sidebarElementWithIndex({ label: 'LDAP' }, ['integrations/ldap/org']), + sidebarElementWithIndex({ label: 'Okta' }, ['integrations/okta/org']), ], ), sidebarElementWithIndex( From fcaac3b8bd45d1f6a42a8ebea0d4cd02eb13cad3 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 13 Mar 2026 17:07:52 +0000 Subject: [PATCH 070/188] Fix Card onPress event Signed-off-by: Charles de Dreuille --- .changeset/tidy-friends-act.md | 7 +++++++ packages/ui/src/components/Card/Card.tsx | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/tidy-friends-act.md diff --git a/.changeset/tidy-friends-act.md b/.changeset/tidy-friends-act.md new file mode 100644 index 0000000000..6db6b77083 --- /dev/null +++ b/.changeset/tidy-friends-act.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Fixed `Card` interactive cards not firing the `onPress` handler when clicking the card surface. + +**Affected components**: Card diff --git a/packages/ui/src/components/Card/Card.tsx b/packages/ui/src/components/Card/Card.tsx index 70a2438a49..aecca09512 100644 --- a/packages/ui/src/components/Card/Card.tsx +++ b/packages/ui/src/components/Card/Card.tsx @@ -77,7 +77,7 @@ export const Card = forwardRef((props, ref) => { triggerRef.current.dispatchEvent( new MouseEvent('click', { - bubbles: false, + bubbles: true, cancelable: true, ctrlKey: e.ctrlKey, metaKey: e.metaKey, From 690786f84d0d85272002e1e4076ada3fb015236e Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Thu, 12 Mar 2026 15:48:18 +0000 Subject: [PATCH 071/188] feat(ui): replace Table loading text with skeleton rows Add a skeleton loading state to the BUI Table component that shows the table header with animated skeleton rows instead of plain "Loading..." text. Includes accessibility support via aria-busy and live region announcements. BUCKS-2919 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Jonathan Roebuck --- .changeset/bui-table-skeleton-loading.md | 5 + .../Table/components/Table.test.tsx | 107 ++++++++++++++++++ .../src/components/Table/components/Table.tsx | 85 +++++++------- .../Table/components/TableBodySkeleton.tsx | 59 ++++++++++ packages/ui/src/setupTests.ts | 16 +++ 5 files changed, 228 insertions(+), 44 deletions(-) create mode 100644 .changeset/bui-table-skeleton-loading.md create mode 100644 packages/ui/src/components/Table/components/Table.test.tsx create mode 100644 packages/ui/src/components/Table/components/TableBodySkeleton.tsx create mode 100644 packages/ui/src/setupTests.ts diff --git a/.changeset/bui-table-skeleton-loading.md b/.changeset/bui-table-skeleton-loading.md new file mode 100644 index 0000000000..a2e050e6b5 --- /dev/null +++ b/.changeset/bui-table-skeleton-loading.md @@ -0,0 +1,5 @@ +--- +'@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. diff --git a/packages/ui/src/components/Table/components/Table.test.tsx b/packages/ui/src/components/Table/components/Table.test.tsx new file mode 100644 index 0000000000..355c780413 --- /dev/null +++ b/packages/ui/src/components/Table/components/Table.test.tsx @@ -0,0 +1,107 @@ +/* + * 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 { Table } from './Table'; +import { CellText } from './CellText'; +import type { ColumnConfig } from '../types'; + +type TestItem = { id: number; name: string; type: string }; + +const testColumns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, +]; + +describe('Table', () => { + describe('loading state', () => { + it('renders a table with header and skeleton rows when loading with no data', async () => { + render( +
, + ); + + const table = screen.getByRole('grid'); + expect(table).toBeInTheDocument(); + + // Header columns should be visible + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); + + // Should render 5 skeleton rows (plus 1 header row = 6 total) + const rows = screen.getAllByRole('row'); + expect(rows).toHaveLength(6); + + // Table should indicate it's in a loading/stale state via data-stale + // (react-aria-components Table renders this as a data attribute rather + // than aria-busy on the DOM element) + expect(table).toHaveAttribute('data-stale', 'true'); + + // Each skeleton row should contain Skeleton placeholder elements + // (5 rows * 2 columns = 10 skeleton divs) + const skeletonElements = table.querySelectorAll('.bui-Skeleton'); + expect(skeletonElements).toHaveLength(10); + + // Skeleton elements should have varying widths for visual variety + const widths = Array.from(skeletonElements).map( + el => (el as HTMLElement).style.width, + ); + expect(new Set(widths).size).toBeGreaterThan(1); + + // Should NOT render "Loading..." text - skeleton is purely visual + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + }); + + it('renders data rows normally when not loading', async () => { + const data: TestItem[] = [ + { id: 1, name: 'Service A', type: 'service' }, + { id: 2, name: 'Library B', type: 'library' }, + ]; + + render( +
, + ); + + expect(screen.getByText('Service A')).toBeInTheDocument(); + expect(screen.getByText('Library B')).toBeInTheDocument(); + + const table = screen.getByRole('grid'); + // When not loading, data-stale should be "false" + expect(table).toHaveAttribute('data-stale', 'false'); + + // Should not contain skeleton elements + const skeletonElements = table.querySelectorAll('.bui-Skeleton'); + expect(skeletonElements).toHaveLength(0); + }); + }); +}); diff --git a/packages/ui/src/components/Table/components/Table.tsx b/packages/ui/src/components/Table/components/Table.tsx index 523e5bd6c3..af9ff3f40c 100644 --- a/packages/ui/src/components/Table/components/Table.tsx +++ b/packages/ui/src/components/Table/components/Table.tsx @@ -32,6 +32,7 @@ import type { import { useMemo } from 'react'; import { VisuallyHidden } from '../../VisuallyHidden'; import { Flex } from '../../Flex'; +import { TableBodySkeleton } from './TableBodySkeleton'; function isRowRenderFn( rowConfig: RowConfig | RowRenderFn | undefined, @@ -115,13 +116,7 @@ export function Table({ onSelectionChange, } = selection || {}; - if (loading && !data) { - return ( -
- Loading... -
- ); - } + const isInitialLoading = loading && !data; if (error) { return ( @@ -131,11 +126,9 @@ export function Table({ ); } - const liveRegionLabel = useLiveRegionLabel( - pagination, - isStale, - data !== undefined, - ); + const liveRegionLabel = isInitialLoading + ? 'Loading table data.' + : useLiveRegionLabel(pagination, isStale, data !== undefined); const manualColumnSizing = columnConfig.some( col => @@ -165,7 +158,7 @@ export function Table({ sortDescriptor={sort?.descriptor ?? undefined} onSortChange={sort?.onSortChange} disabledKeys={disabledRows} - stale={isStale} + stale={isStale || isInitialLoading} aria-describedby={liveRegionId} > @@ -187,39 +180,43 @@ export function Table({ ) } - {emptyState} : undefined - } - > - {item => { - const itemIndex = data?.indexOf(item) ?? -1; - - if (isRowRenderFn(rowConfig)) { - return rowConfig({ - item, - index: itemIndex, - }); + {isInitialLoading ? ( + + ) : ( + {emptyState} : undefined } + > + {item => { + const itemIndex = data?.indexOf(item) ?? -1; - return ( - rowConfig?.onClick?.(item) - : undefined - } - > - {column => column.cell(item)} - - ); - }} - + if (isRowRenderFn(rowConfig)) { + return rowConfig({ + item, + index: itemIndex, + }); + } + + return ( + rowConfig?.onClick?.(item) + : undefined + } + > + {column => column.cell(item)} + + ); + }} + + )} , )} {pagination.type === 'page' && ( diff --git a/packages/ui/src/components/Table/components/TableBodySkeleton.tsx b/packages/ui/src/components/Table/components/TableBodySkeleton.tsx new file mode 100644 index 0000000000..1623fdc9cf --- /dev/null +++ b/packages/ui/src/components/Table/components/TableBodySkeleton.tsx @@ -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 { TableBody } from './TableBody'; +import { Row } from './Row'; +import { Cell } from './Cell'; +import { Skeleton } from '../../Skeleton'; +import type { ColumnConfig, TableItem } from '../types'; + +const SKELETON_ROW_COUNT = 5; +const SKELETON_WIDTHS = ['75%', '50%', '60%', '45%', '70%']; + +const skeletonItems = Array.from({ length: SKELETON_ROW_COUNT }, (_, i) => ({ + id: `skeleton-${i}`, +})); + +/** @internal */ +export function TableBodySkeleton({ + columns, +}: { + columns: readonly ColumnConfig[]; +}) { + return ( + + {item => { + const rowIndex = Number(item.id.split('-')[1]); + return ( + + {column => ( + + )} + + ); + }} + + ); +} diff --git a/packages/ui/src/setupTests.ts b/packages/ui/src/setupTests.ts new file mode 100644 index 0000000000..5f11bab1c2 --- /dev/null +++ b/packages/ui/src/setupTests.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. + */ +import '@testing-library/jest-dom'; From 6f56a0c0b05d49c2cf8df9f055e996323dc6bcbe Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Thu, 12 Mar 2026 16:40:29 +0000 Subject: [PATCH 072/188] fix(ui): address review feedback for Table skeleton loading - Add @testing-library/jest-dom and @testing-library/react as explicit devDependencies for @backstage/ui - Disable selection on TableRoot during initial loading to prevent interaction with skeleton rows - Clarify test comment about aria-busy vs data-stale behavior Co-Authored-By: Claude Opus 4.6 Signed-off-by: Jonathan Roebuck --- packages/ui/package.json | 2 ++ .../ui/src/components/Table/components/Table.test.tsx | 4 ++-- packages/ui/src/components/Table/components/Table.tsx | 8 ++++---- yarn.lock | 2 ++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index a1df4bcf8a..ce979d58d2 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -55,6 +55,8 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^16.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/use-sync-external-store": "^0.0.6", diff --git a/packages/ui/src/components/Table/components/Table.test.tsx b/packages/ui/src/components/Table/components/Table.test.tsx index 355c780413..f10d672af4 100644 --- a/packages/ui/src/components/Table/components/Table.test.tsx +++ b/packages/ui/src/components/Table/components/Table.test.tsx @@ -59,8 +59,8 @@ describe('Table', () => { expect(rows).toHaveLength(6); // Table should indicate it's in a loading/stale state via data-stale - // (react-aria-components Table renders this as a data attribute rather - // than aria-busy on the DOM element) + // (react-aria-components' Table does not forward aria-busy to the DOM, + // but the stale prop produces a data-stale attribute for CSS targeting) expect(table).toHaveAttribute('data-stale', 'true'); // Each skeleton row should contain Skeleton placeholder elements diff --git a/packages/ui/src/components/Table/components/Table.tsx b/packages/ui/src/components/Table/components/Table.tsx index af9ff3f40c..e5f519b8e5 100644 --- a/packages/ui/src/components/Table/components/Table.tsx +++ b/packages/ui/src/components/Table/components/Table.tsx @@ -151,10 +151,10 @@ export function Table({ {wrapResizable( Date: Thu, 12 Mar 2026 16:58:28 +0000 Subject: [PATCH 073/188] chore(ui): remove Table tests and test setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed per reviewer feedback — BUI Table relies on Storybook visual tests rather than unit tests. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Jonathan Roebuck --- packages/ui/package.json | 2 - .../Table/components/Table.test.tsx | 107 ------------------ packages/ui/src/setupTests.ts | 16 --- yarn.lock | 2 - 4 files changed, 127 deletions(-) delete mode 100644 packages/ui/src/components/Table/components/Table.test.tsx delete mode 100644 packages/ui/src/setupTests.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index ce979d58d2..a1df4bcf8a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -55,8 +55,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@testing-library/jest-dom": "^6.0.0", - "@testing-library/react": "^16.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/use-sync-external-store": "^0.0.6", diff --git a/packages/ui/src/components/Table/components/Table.test.tsx b/packages/ui/src/components/Table/components/Table.test.tsx deleted file mode 100644 index f10d672af4..0000000000 --- a/packages/ui/src/components/Table/components/Table.test.tsx +++ /dev/null @@ -1,107 +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 { render, screen } from '@testing-library/react'; -import { Table } from './Table'; -import { CellText } from './CellText'; -import type { ColumnConfig } from '../types'; - -type TestItem = { id: number; name: string; type: string }; - -const testColumns: ColumnConfig[] = [ - { - id: 'name', - label: 'Name', - isRowHeader: true, - cell: item => , - }, - { - id: 'type', - label: 'Type', - cell: item => , - }, -]; - -describe('Table', () => { - describe('loading state', () => { - it('renders a table with header and skeleton rows when loading with no data', async () => { - render( -
, - ); - - const table = screen.getByRole('grid'); - expect(table).toBeInTheDocument(); - - // Header columns should be visible - expect(screen.getByText('Name')).toBeInTheDocument(); - expect(screen.getByText('Type')).toBeInTheDocument(); - - // Should render 5 skeleton rows (plus 1 header row = 6 total) - const rows = screen.getAllByRole('row'); - expect(rows).toHaveLength(6); - - // Table should indicate it's in a loading/stale state via data-stale - // (react-aria-components' Table does not forward aria-busy to the DOM, - // but the stale prop produces a data-stale attribute for CSS targeting) - expect(table).toHaveAttribute('data-stale', 'true'); - - // Each skeleton row should contain Skeleton placeholder elements - // (5 rows * 2 columns = 10 skeleton divs) - const skeletonElements = table.querySelectorAll('.bui-Skeleton'); - expect(skeletonElements).toHaveLength(10); - - // Skeleton elements should have varying widths for visual variety - const widths = Array.from(skeletonElements).map( - el => (el as HTMLElement).style.width, - ); - expect(new Set(widths).size).toBeGreaterThan(1); - - // Should NOT render "Loading..." text - skeleton is purely visual - expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); - }); - - it('renders data rows normally when not loading', async () => { - const data: TestItem[] = [ - { id: 1, name: 'Service A', type: 'service' }, - { id: 2, name: 'Library B', type: 'library' }, - ]; - - render( -
, - ); - - expect(screen.getByText('Service A')).toBeInTheDocument(); - expect(screen.getByText('Library B')).toBeInTheDocument(); - - const table = screen.getByRole('grid'); - // When not loading, data-stale should be "false" - expect(table).toHaveAttribute('data-stale', 'false'); - - // Should not contain skeleton elements - const skeletonElements = table.querySelectorAll('.bui-Skeleton'); - expect(skeletonElements).toHaveLength(0); - }); - }); -}); diff --git a/packages/ui/src/setupTests.ts b/packages/ui/src/setupTests.ts deleted file mode 100644 index 5f11bab1c2..0000000000 --- a/packages/ui/src/setupTests.ts +++ /dev/null @@ -1,16 +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. - */ -import '@testing-library/jest-dom'; diff --git a/yarn.lock b/yarn.lock index 52709955fb..fbc65dcfdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7652,8 +7652,6 @@ __metadata: "@backstage/version-bridge": "workspace:^" "@remixicon/react": "npm:^4.6.0" "@tanstack/react-table": "npm:^8.21.3" - "@testing-library/jest-dom": "npm:^6.0.0" - "@testing-library/react": "npm:^16.0.0" "@types/react": "npm:^18.0.0" "@types/react-dom": "npm:^18.0.0" "@types/use-sync-external-store": "npm:^0.0.6" From 4bd75da4cf0b2f80ad76bd915c0c5d1db28e6ac4 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Thu, 12 Mar 2026 17:18:18 +0000 Subject: [PATCH 074/188] docs(ui): fix changeset wording for Table loading state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove inaccurate aria-busy claim from changeset — react-aria-components does not forward it to the DOM. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Jonathan Roebuck --- .changeset/bui-table-skeleton-loading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/bui-table-skeleton-loading.md b/.changeset/bui-table-skeleton-loading.md index a2e050e6b5..4a2d8aaea4 100644 --- a/.changeset/bui-table-skeleton-loading.md +++ b/.changeset/bui-table-skeleton-loading.md @@ -2,4 +2,4 @@ '@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. +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 accessibility support via a screen reader live-region announcement. From 36d9e1b56e462f08735f505db839a1317d168a97 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Fri, 13 Mar 2026 09:44:05 +0000 Subject: [PATCH 075/188] docs(ui): update changeset with affected component Signed-off-by: Jonathan Roebuck --- .changeset/bui-table-skeleton-loading.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/bui-table-skeleton-loading.md b/.changeset/bui-table-skeleton-loading.md index 4a2d8aaea4..df20393c50 100644 --- a/.changeset/bui-table-skeleton-loading.md +++ b/.changeset/bui-table-skeleton-loading.md @@ -2,4 +2,6 @@ '@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 accessibility support via a screen reader live-region announcement. +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 From a688c806bcae348f65f54103d371db629a2d676e Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Fri, 13 Mar 2026 09:57:25 +0000 Subject: [PATCH 076/188] refactor(ui): consolidate live region label logic Move initial loading label into useLiveRegionLabel so all live region logic is in one place. Signed-off-by: Jonathan Roebuck --- .../ui/src/components/Table/components/Table.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/Table/components/Table.tsx b/packages/ui/src/components/Table/components/Table.tsx index e5f519b8e5..6ad1fa96ae 100644 --- a/packages/ui/src/components/Table/components/Table.tsx +++ b/packages/ui/src/components/Table/components/Table.tsx @@ -62,8 +62,13 @@ function useDisabledRows({ function useLiveRegionLabel( pagination: TablePaginationType, isStale: boolean, + isLoading: boolean, hasData: boolean, ): string { + if (isLoading) { + return 'Loading table data.'; + } + if (!hasData || pagination.type === 'none') { return ''; } @@ -126,9 +131,12 @@ export function Table({ ); } - const liveRegionLabel = isInitialLoading - ? 'Loading table data.' - : useLiveRegionLabel(pagination, isStale, data !== undefined); + const liveRegionLabel = useLiveRegionLabel( + pagination, + isStale, + isInitialLoading, + data !== undefined, + ); const manualColumnSizing = columnConfig.some( col => From 3ace9c54db6dca0d0a114b0782bba7cd14a90fe2 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Fri, 13 Mar 2026 09:57:41 +0000 Subject: [PATCH 077/188] feat(ui): add separate loading prop to TableRoot Use spread for conditional selection props. Add a dedicated loading prop and data-loading data attribute to TableRoot, distinct from stale. Both set aria-busy and default to the same opacity, but consumers can now style them independently. Signed-off-by: Jonathan Roebuck --- packages/ui/report.api.md | 4 ++++ packages/ui/src/components/Table/Table.module.css | 3 ++- .../ui/src/components/Table/components/Table.tsx | 15 ++++++++++----- .../src/components/Table/components/TableRoot.tsx | 2 +- packages/ui/src/components/Table/definition.ts | 1 + packages/ui/src/components/Table/types.ts | 1 + 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 834251f088..f2d326dc30 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -2332,6 +2332,9 @@ export const TableDefinition: { readonly stale: { readonly dataAttribute: true; }; + readonly loading: { + readonly dataAttribute: true; + }; }; }; @@ -2449,6 +2452,7 @@ export const TableRoot: (props: TableRootProps) => JSX_2.Element; // @public (undocumented) export type TableRootOwnProps = { stale?: boolean; + loading?: boolean; }; // @public (undocumented) diff --git a/packages/ui/src/components/Table/Table.module.css b/packages/ui/src/components/Table/Table.module.css index 55cf235e56..472e0733ac 100644 --- a/packages/ui/src/components/Table/Table.module.css +++ b/packages/ui/src/components/Table/Table.module.css @@ -24,7 +24,8 @@ table-layout: fixed; transition: opacity 0.2s ease-in-out; - &[data-stale='true'] { + &[data-stale='true'], + &[data-loading='true'] { opacity: 0.6; } } diff --git a/packages/ui/src/components/Table/components/Table.tsx b/packages/ui/src/components/Table/components/Table.tsx index 6ad1fa96ae..96e7cfe166 100644 --- a/packages/ui/src/components/Table/components/Table.tsx +++ b/packages/ui/src/components/Table/components/Table.tsx @@ -159,14 +159,19 @@ export function Table({ {wrapResizable( diff --git a/packages/ui/src/components/Table/components/TableRoot.tsx b/packages/ui/src/components/Table/components/TableRoot.tsx index af96d1809e..223e9d648b 100644 --- a/packages/ui/src/components/Table/components/TableRoot.tsx +++ b/packages/ui/src/components/Table/components/TableRoot.tsx @@ -30,7 +30,7 @@ export const TableRoot = (props: TableRootProps) => { diff --git a/packages/ui/src/components/Table/definition.ts b/packages/ui/src/components/Table/definition.ts index eb41617b93..1c3953a19a 100644 --- a/packages/ui/src/components/Table/definition.ts +++ b/packages/ui/src/components/Table/definition.ts @@ -38,6 +38,7 @@ export const TableDefinition = defineComponent()({ }, propDefs: { stale: { dataAttribute: true }, + loading: { dataAttribute: true }, }, }); diff --git a/packages/ui/src/components/Table/types.ts b/packages/ui/src/components/Table/types.ts index 099a855d33..0736afa05b 100644 --- a/packages/ui/src/components/Table/types.ts +++ b/packages/ui/src/components/Table/types.ts @@ -42,6 +42,7 @@ export interface SortState { /** @public */ export type TableRootOwnProps = { stale?: boolean; + loading?: boolean; }; /** @public */ From b838cc97b0f42b98d05b12e9eaa72be7cca3af92 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Fri, 13 Mar 2026 11:52:08 +0000 Subject: [PATCH 078/188] docs(ui): add changeset and docs for TableRoot loading prop Signed-off-by: Jonathan Roebuck --- .changeset/bui-table-root-loading-prop.md | 7 +++++++ docs-ui/src/app/components/table/props-definition.tsx | 11 +++++++++++ 2 files changed, 18 insertions(+) create mode 100644 .changeset/bui-table-root-loading-prop.md diff --git a/.changeset/bui-table-root-loading-prop.md b/.changeset/bui-table-root-loading-prop.md new file mode 100644 index 0000000000..b5728154cc --- /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: Table diff --git a/docs-ui/src/app/components/table/props-definition.tsx b/docs-ui/src/app/components/table/props-definition.tsx index abfdd45549..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 = { From 5d4fc274a2ac7e5029669ad066a0fbc59dced21b Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Fri, 13 Mar 2026 16:27:26 +0000 Subject: [PATCH 079/188] Update .changeset/bui-table-root-loading-prop.md Co-authored-by: Johan Persson Signed-off-by: Jonathan Roebuck --- .changeset/bui-table-root-loading-prop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/bui-table-root-loading-prop.md b/.changeset/bui-table-root-loading-prop.md index b5728154cc..7c3f453c82 100644 --- a/.changeset/bui-table-root-loading-prop.md +++ b/.changeset/bui-table-root-loading-prop.md @@ -4,4 +4,4 @@ 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: Table +Affected components: TableRoot From 434b0284a396950f9211bdbbe09a951417927f53 Mon Sep 17 00:00:00 2001 From: chanchalkhatri Date: Sat, 14 Mar 2026 02:01:33 +0530 Subject: [PATCH 080/188] fix(microsite): correct blog typos and malformed punctuation (#33289) Signed-off-by: chanchalkhatri19 --- microsite/blog/2020-03-18-what-is-backstage.mdx | 2 +- microsite/blog/2023-04-26-kubecon-eu-2023.mdx | 2 +- ...-chicago-traiding-company-adopter-spotlight.mdx | 14 +++++++------- .../blog/2025-12-30-backstage-wrapped-2025.mdx | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) 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-09-29-chicago-traiding-company-adopter-spotlight.mdx b/microsite/blog/2023-09-29-chicago-traiding-company-adopter-spotlight.mdx index 169c907e40..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,11 +49,11 @@ 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). 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]. From 9b484468649ecdc92440b0eac55b6bfa6d626c80 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 13 Mar 2026 16:38:16 -0500 Subject: [PATCH 081/188] Added new AI section Co-authored-by: Nawaraj Signed-off-by: Andre Wanlin --- docs/ai/index.md | 256 ++++++++++++++++++++++++++++++++++ docs/ai/well-known-actions.md | 29 ++++ microsite/sidebars.ts | 8 ++ 3 files changed, 293 insertions(+) create mode 100644 docs/ai/index.md create mode 100644 docs/ai/well-known-actions.md diff --git a/docs/ai/index.md b/docs/ai/index.md new file mode 100644 index 0000000000..f8ac68c438 --- /dev/null +++ b/docs/ai/index.md @@ -0,0 +1,256 @@ +--- +id: index +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 + +Make sure to provide the list of plugins from which you want exposed as MCP tools by populating the `pluginSources` configuration: + +```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 Sever Name & Description + +You can configure the name and description of the MCP Actions server with the following config for a single server, see Multiple MCP Server: + +```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..6788f93548 --- /dev/null +++ b/docs/ai/well-known-actions.md @@ -0,0 +1,29 @@ +--- +id: well-known-actions +title: Well-known Actions +description: This section lists a number of well known actions that are a part of the action 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. Entires are in the format: "`action-name` (Action Title): Shortened Action Description" + +### 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 + +- `get-catalog-entity` (Get Catalog Entity): This allows you to get a single entity from the software catalog. +- `query-catalog-entities` (Query Catalog Entities): Query entities from the Backstage Software Catalog using predicate filters. +- `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. +- `unregister-entity` (Unregister entity from the Catalog): Unregisters a Location entity and all entities it owns from the Backstage 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 + +- `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. +- `list-scaffolder-actions` (List Scaffolder Actions): Lists all installed Scaffolder actions. +- `list-scaffolder-tasks` (List Scaffolder Tasks): This allows you to list scaffolder tasks that have been created. diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 7e4e41591b..f6e5b23b29 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -113,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/index', 'ai/well-known-actions'], + ), sidebarElementWithIndex( { label: 'Auth and Identity', From 5abb339d5705755b0f1139c1bb2c7b7bec1fd39f Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 13 Mar 2026 16:42:16 -0500 Subject: [PATCH 082/188] Fixed typo Signed-off-by: Andre Wanlin --- docs/ai/well-known-actions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ai/well-known-actions.md b/docs/ai/well-known-actions.md index 6788f93548..eda490c77c 100644 --- a/docs/ai/well-known-actions.md +++ b/docs/ai/well-known-actions.md @@ -8,7 +8,7 @@ This section lists a number of well known [Actions](../backend-system/core-servi ## Actions -This is a (non-exhaustive) list of actions that are known to be part of the actions registry. Entires are in the format: "`action-name` (Action Title): Shortened Action Description" +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 From f3a91009324c24f574041ec60b4285bb45465ba1 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 13 Mar 2026 16:47:40 -0500 Subject: [PATCH 083/188] Feedback corrections Signed-off-by: Andre Wanlin --- docs/ai/index.md | 16 ++++++++-------- docs/ai/well-known-actions.md | 2 +- microsite/sidebars.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/ai/index.md b/docs/ai/index.md index f8ac68c438..b6f87e22cd 100644 --- a/docs/ai/index.md +++ b/docs/ai/index.md @@ -4,7 +4,7 @@ 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. +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 @@ -38,11 +38,11 @@ backend: For details on filtering actions, see the [Filtering actions documentation](../backend-system/core-services/actions.md#filtering-actions). -## Single MCP Sever Name & Description +## Single MCP Server Name & Description -You can configure the name and description of the MCP Actions server with the following config for a single server, see Multiple MCP Server: +You can configure the name and description of the MCP Actions server with the following config for a single server: -```yaml title="app-config.yaml +```yaml title="app-config.yaml" mcpActions: name: 'My MCP Server' # defaults to "backstage" description: 'Tools for interacting with My MCP Server' # optional @@ -54,7 +54,7 @@ By default, MCP tool names include the plugin ID prefix to avoid collisions acro You can disable this if you need the short names for backward compatibility: -```yaml title="app-config.yaml +```yaml title="app-config.yaml" mcpActions: namespacedToolNames: false ``` @@ -63,7 +63,7 @@ mcpActions: 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 +```yaml title="app-config.yaml" mcpActions: servers: catalog: @@ -93,7 +93,7 @@ When `mcpActions.servers` is not configured, the plugin behaves exactly as befor 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 +```yaml title="app-config.yaml" mcpActions: servers: catalog: @@ -118,7 +118,7 @@ This is meant to be a temporary workaround until device authentication is comple Configure external access with static tokens in your app configuration: -```yaml title="app-config.yaml +```yaml title="app-config.yaml" backend: auth: externalAccess: diff --git a/docs/ai/well-known-actions.md b/docs/ai/well-known-actions.md index eda490c77c..50a5d6404c 100644 --- a/docs/ai/well-known-actions.md +++ b/docs/ai/well-known-actions.md @@ -4,7 +4,7 @@ title: Well-known Actions description: This section lists a number of well known actions that are a part of the action 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). +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 diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index f6e5b23b29..25e470f949 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -117,7 +117,7 @@ export default { { label: 'AI', description: - 'Features in Backstage you can leverage with your AI tools', + 'Features in Backstage you can leverage with your AI tools.', }, ['ai/index', 'ai/well-known-actions'], ), From 8aebd31e345e8c75f89918016f4944bc8d91fb27 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 13 Mar 2026 17:02:42 -0500 Subject: [PATCH 084/188] Minor adjustments Signed-off-by: Andre Wanlin --- docs/ai/well-known-actions.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/ai/well-known-actions.md b/docs/ai/well-known-actions.md index 50a5d6404c..14039214d4 100644 --- a/docs/ai/well-known-actions.md +++ b/docs/ai/well-known-actions.md @@ -1,29 +1,29 @@ --- id: well-known-actions title: Well-known Actions -description: This section lists a number of well known actions that are a part of the action registry. +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). +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" +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 -- `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. +- `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 -- `get-catalog-entity` (Get Catalog Entity): This allows you to get a single entity from the software catalog. -- `query-catalog-entities` (Query Catalog Entities): Query entities from the Backstage Software Catalog using predicate filters. -- `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. -- `unregister-entity` (Unregister entity from the Catalog): Unregisters a Location entity and all entities it owns from the Backstage 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. +- `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 -- `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. -- `list-scaffolder-actions` (List Scaffolder Actions): Lists all installed Scaffolder actions. -- `list-scaffolder-tasks` (List Scaffolder Tasks): This allows you to list scaffolder tasks that have been created. +- `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. From 8c19ddbadfda49d4098e45e3dec9c673caa88763 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:56:04 -0500 Subject: [PATCH 085/188] Update docs/ai/index.md Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- docs/ai/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ai/index.md b/docs/ai/index.md index b6f87e22cd..535a322f4a 100644 --- a/docs/ai/index.md +++ b/docs/ai/index.md @@ -36,7 +36,7 @@ backend: - 'my-custom-plugin' ``` -For details on filtering actions, see the [Filtering actions documentation](../backend-system/core-services/actions.md#filtering-actions). +For details on filtering actions, see the [filtering actions documentation](../backend-system/core-services/actions.md#filtering-actions). ## Single MCP Server Name & Description From 34d53159918293f6a19de52bd5d93966e0723cb2 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:56:30 -0500 Subject: [PATCH 086/188] Update docs/ai/index.md Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- docs/ai/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ai/index.md b/docs/ai/index.md index 535a322f4a..d45b0f9ba8 100644 --- a/docs/ai/index.md +++ b/docs/ai/index.md @@ -26,7 +26,7 @@ backend.start(); ## Actions Configuration -Make sure to provide the list of plugins from which you want exposed as MCP tools by populating the `pluginSources` configuration: +Populate the `pluginSources` configuration with the list of plugins you want exposed as MCP tools like so: ```yaml backend: From a148a319df2a961c811af66d855b191e0121d2f0 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:56:43 -0500 Subject: [PATCH 087/188] Update docs/ai/index.md Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- docs/ai/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ai/index.md b/docs/ai/index.md index d45b0f9ba8..111f11d265 100644 --- a/docs/ai/index.md +++ b/docs/ai/index.md @@ -14,7 +14,7 @@ This plugin is installed via the `@backstage/plugin-mcp-actions-backend` package yarn --cwd packages/backend add @backstage/plugin-mcp-actions-backend ``` -Then add the plugin to your backend: +Then, add the plugin to your backend: ```ts title="packages/backend/src/index.ts" const backend = createBackend(); From e4cd6aad707b8a91c82e824f25bc46e4b7d864bb Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:57:09 -0500 Subject: [PATCH 088/188] Update docs/ai/index.md Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- docs/ai/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ai/index.md b/docs/ai/index.md index 111f11d265..e973456988 100644 --- a/docs/ai/index.md +++ b/docs/ai/index.md @@ -40,7 +40,7 @@ For details on filtering actions, see the [filtering actions documentation](../b ## Single MCP Server Name & Description -You can configure the name and description of the MCP Actions server with the following config for a single server: +You can configure the name and description of your Backstage MCP server with the following config: ```yaml title="app-config.yaml" mcpActions: From 52fba1c57e5516ba22ba521494201a7a560b6a9b Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 13 Mar 2026 17:59:21 -0500 Subject: [PATCH 089/188] Review feedback Signed-off-by: Andre Wanlin --- docs/ai/{index.md => mcp-actions.md} | 2 +- microsite/sidebars.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/ai/{index.md => mcp-actions.md} (99%) diff --git a/docs/ai/index.md b/docs/ai/mcp-actions.md similarity index 99% rename from docs/ai/index.md rename to docs/ai/mcp-actions.md index e973456988..498cd42df2 100644 --- a/docs/ai/index.md +++ b/docs/ai/mcp-actions.md @@ -1,5 +1,5 @@ --- -id: index +id: mcp-actions title: MCP Actions Backend description: The MCP Actions Backend exposes actions registered with the Actions Registry as MCP tools. --- diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 25e470f949..9e56faac21 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -119,7 +119,7 @@ export default { description: 'Features in Backstage you can leverage with your AI tools.', }, - ['ai/index', 'ai/well-known-actions'], + ['ai/mcp-actions', 'ai/well-known-actions'], ), sidebarElementWithIndex( { From 7e1f0f0a09b1d7a314663f0b6e1e2d9d62f3bc49 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 23:32:28 +0000 Subject: [PATCH 090/188] chore(deps): update dependency undici to v7.24.0 [security] Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index fbc65dcfdf..d8666db870 100644 --- a/yarn.lock +++ b/yarn.lock @@ -49082,9 +49082,9 @@ __metadata: linkType: hard "undici@npm:^7.2.3, undici@npm:^7.22.0": - version: 7.22.0 - resolution: "undici@npm:7.22.0" - checksum: 10/a7a1813ba4b74c0d46cc8dd160386202c05699ffc487c5d882cf40e6d2435c8d6faff3b8f8675d09bd1ef0386e370675c26b59b9a8c8b3f17b9f82a42236a927 + version: 7.24.1 + resolution: "undici@npm:7.24.1" + checksum: 10/0af4ec2fc2ae50b1d0636895793ea2274a89a7cd445690089a1b957c4ac4a0336e48ab9ac48a4e8f5023d4b5eab56368c3f17c75d6b4c81b4281b674d1e60c9f languageName: node linkType: hard From 527d055a78a491a51fe137829eb1dbc9cb78894b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 00:58:20 +0000 Subject: [PATCH 091/188] chore(deps): update dependency yauzl to v3.2.1 [security] Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d8666db870..32b09cc5e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -50860,12 +50860,12 @@ __metadata: 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 From b99f6d5a9360ec04a41ebf5a3df35a95cbbdf549 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sat, 14 Mar 2026 07:18:06 +0000 Subject: [PATCH 092/188] fix(ui): fix Dialog content overflowing when no height prop is set Replaces the invalid `min(auto, calc(100vh - 3rem))` CSS pattern with a flex-based layout. The dialog now grows with its content by default and scrolls when the content exceeds the viewport height. A fixed height can still be applied via the `height` prop. Signed-off-by: Charles de Dreuille Made-with: Cursor --- .changeset/fix-dialog-height-overflow.md | 7 ++++ .../src/components/Dialog/Dialog.module.css | 9 +++-- .../src/components/Dialog/Dialog.stories.tsx | 33 +++++++++++++++++++ packages/ui/src/components/Dialog/Dialog.tsx | 11 ++++--- 4 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 .changeset/fix-dialog-height-overflow.md diff --git a/.changeset/fix-dialog-height-overflow.md b/.changeset/fix-dialog-height-overflow.md new file mode 100644 index 0000000000..bf91d882dd --- /dev/null +++ b/.changeset/fix-dialog-height-overflow.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Fixed `Dialog` content overflowing when no `height` prop is set. The dialog now grows with its content and scrolls when content exceeds the viewport height. + +**Affected components**: Dialog diff --git a/packages/ui/src/components/Dialog/Dialog.module.css b/packages/ui/src/components/Dialog/Dialog.module.css index da82509134..194128ec4d 100644 --- a/packages/ui/src/components/Dialog/Dialog.module.css +++ b/packages/ui/src/components/Dialog/Dialog.module.css @@ -50,9 +50,11 @@ border: 1px solid var(--bui-border-1); color: var(--bui-fg-primary); position: relative; + display: flex; + flex-direction: column; 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)); + height: var(--bui-dialog-height, auto); max-height: calc(100vh - 3rem); outline: none; } @@ -61,7 +63,9 @@ display: flex; flex-direction: column; border-radius: var(--dialog-border-radius); - height: 100%; + flex: 1; + min-height: 0; + overflow: hidden; } /* Dialog entering animation */ @@ -102,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 9084bfa7e4..8892cb2314 100644 --- a/packages/ui/src/components/Dialog/Dialog.stories.tsx +++ b/packages/ui/src/components/Dialog/Dialog.stories.tsx @@ -247,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 fad5e10ea2..3a23ee27c9 100644 --- a/packages/ui/src/components/Dialog/Dialog.tsx +++ b/packages/ui/src/components/Dialog/Dialog.tsx @@ -67,11 +67,12 @@ export const Dialog = forwardRef, DialogProps>( style={{ ['--bui-dialog-min-width' as keyof React.CSSProperties]: typeof width === 'number' ? `${width}px` : width || '400px', - ['--bui-dialog-min-height' as keyof React.CSSProperties]: height - ? typeof height === 'number' - ? `${height}px` - : height - : 'auto', + ...(height + ? { + ['--bui-dialog-height' as keyof React.CSSProperties]: + typeof height === 'number' ? `${height}px` : height, + } + : {}), ...style, }} > From 17d6398e221e2f2d9b853f2797dbf768419e99ed Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sat, 14 Mar 2026 10:22:13 +0000 Subject: [PATCH 093/188] fix(ui): fix Container bottom margin clash in Header - Changed Container default bottom spacing from padding-bottom to margin-bottom - Prevented the Container margin-bottom from applying when used as the Header root element via mb="0" - Renamed Header CSS classes from bui-HeaderPage* to bui-Header* to match the component name Fixes https://linear.app/spotify/issue/BACUI-249 Signed-off-by: Charles de Dreuille Made-with: Cursor --- .changeset/fix-header-container-padding.md | 5 +++++ .changeset/rename-header-css-classes.md | 15 ++++++++++++++ packages/ui/report.api.md | 20 +++++++++---------- .../components/Container/Container.module.css | 2 +- .../src/components/Header/Header.module.css | 10 +++++----- packages/ui/src/components/Header/Header.tsx | 2 +- .../ui/src/components/Header/definition.ts | 10 +++++----- 7 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 .changeset/fix-header-container-padding.md create mode 100644 .changeset/rename-header-css-classes.md diff --git a/.changeset/fix-header-container-padding.md b/.changeset/fix-header-container-padding.md new file mode 100644 index 0000000000..fdad424ef5 --- /dev/null +++ b/.changeset/fix-header-container-padding.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Fixed incorrect bottom spacing caused by `Container` using `padding-bottom` for its default bottom spacing. Changed to `margin-bottom` and prevented it from applying when `Container` is used as the `Header` root element. diff --git a/.changeset/rename-header-css-classes.md b/.changeset/rename-header-css-classes.md new file mode 100644 index 0000000000..3e57717c8e --- /dev/null +++ b/.changeset/rename-header-css-classes.md @@ -0,0 +1,15 @@ +--- +'@backstage/ui': minor +--- + +**BREAKING**: Renamed internal CSS classes to match the `Header` component name. + +**Migration notes**: If you are targeting these classes directly in your styles, update the following: + +- `bui-HeaderPage` → `bui-Header` +- `bui-HeaderPageContent` → `bui-HeaderContent` +- `bui-HeaderPageBreadcrumbs` → `bui-HeaderBreadcrumbs` +- `bui-HeaderPageTabsWrapper` → `bui-HeaderTabsWrapper` +- `bui-HeaderPageControls` → `bui-HeaderControls` + +**Affected components**: Header diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 834251f088..95bc6d86b8 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -1409,11 +1409,11 @@ export const HeaderDefinition: { 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: {}; @@ -1450,11 +1450,11 @@ export const HeaderPageDefinition: { 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: {}; diff --git a/packages/ui/src/components/Container/Container.module.css b/packages/ui/src/components/Container/Container.module.css index b93665525f..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; - padding-bottom: var(--bui-space-8); + margin-bottom: var(--bui-space-8); } @media (min-width: 640px) { diff --git a/packages/ui/src/components/Header/Header.module.css b/packages/ui/src/components/Header/Header.module.css index 4a81c6dcb0..7f12f5adf8 100644 --- a/packages/ui/src/components/Header/Header.module.css +++ b/packages/ui/src/components/Header/Header.module.css @@ -17,7 +17,7 @@ @layer tokens, base, components, utilities; @layer components { - .bui-HeaderPage { + .bui-Header { display: flex; flex-direction: column; gap: var(--bui-space-1); @@ -25,24 +25,24 @@ margin-bottom: var(--bui-space-6); } - .bui-HeaderPageContent { + .bui-HeaderContent { display: flex; flex-direction: row; justify-content: space-between; } - .bui-HeaderPageTabsWrapper { + .bui-HeaderTabsWrapper { margin-left: -8px; } - .bui-HeaderPageControls { + .bui-HeaderControls { display: flex; flex-direction: row; align-items: center; gap: var(--bui-space-2); } - .bui-HeaderPageBreadcrumbs { + .bui-HeaderBreadcrumbs { display: flex; flex-direction: row; align-items: center; diff --git a/packages/ui/src/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx index d9d96eb7af..139604147a 100644 --- a/packages/ui/src/components/Header/Header.tsx +++ b/packages/ui/src/components/Header/Header.tsx @@ -34,7 +34,7 @@ export const Header = (props: HeaderProps) => { const { classes, title, tabs, customActions, breadcrumbs } = ownProps; return ( - +
{breadcrumbs && diff --git a/packages/ui/src/components/Header/definition.ts b/packages/ui/src/components/Header/definition.ts index 0eb3ad33a3..66fccf2f49 100644 --- a/packages/ui/src/components/Header/definition.ts +++ b/packages/ui/src/components/Header/definition.ts @@ -25,11 +25,11 @@ import styles from './Header.module.css'; export const HeaderDefinition = defineComponent()({ styles, classNames: { - root: 'bui-HeaderPage', - content: 'bui-HeaderPageContent', - breadcrumbs: 'bui-HeaderPageBreadcrumbs', - tabsWrapper: 'bui-HeaderPageTabsWrapper', - controls: 'bui-HeaderPageControls', + root: 'bui-Header', + content: 'bui-HeaderContent', + breadcrumbs: 'bui-HeaderBreadcrumbs', + tabsWrapper: 'bui-HeaderTabsWrapper', + controls: 'bui-HeaderControls', }, propDefs: { title: {}, From 895563a1b7824683ad55dad8b96eb39e1d4db201 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Wed, 7 Jan 2026 15:01:49 +0100 Subject: [PATCH 094/188] fix(techdocs): Disable LighBox addon for images wrapped in links Signed-off-by: Gabriel Dugny # Conflicts: # docs/features/techdocs/addons--new.md # docs/features/techdocs/addons.md # Conflicts: # docs/features/techdocs/addons--new.md --- .changeset/giant-carpets-train.md | 5 +++ docs/features/techdocs/addons--new.md | 12 +++--- docs/features/techdocs/addons.md | 12 +++--- .../src/LightBox/LightBox.test.tsx | 39 +++++++++++++++++++ .../src/LightBox/LightBox.tsx | 5 ++- .../src/plugin.ts | 2 + 6 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 .changeset/giant-carpets-train.md diff --git a/.changeset/giant-carpets-train.md b/.changeset/giant-carpets-train.md new file mode 100644 index 0000000000..aa5aa31a82 --- /dev/null +++ b/.changeset/giant-carpets-train.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-module-addons-contrib': patch +--- + +Avoid enabling the TechDocs LightBox addon for images wrapped in links, so image links keep working. diff --git a/docs/features/techdocs/addons--new.md b/docs/features/techdocs/addons--new.md index af7f4d2893..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.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`). | +| 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/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: * ``` From 4ebcde982660896a623f7181b69d3245e451b27b Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sat, 14 Mar 2026 10:31:24 +0000 Subject: [PATCH 095/188] Update Header.tsx Signed-off-by: Charles de Dreuille --- packages/ui/src/components/Header/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx index 139604147a..d9d96eb7af 100644 --- a/packages/ui/src/components/Header/Header.tsx +++ b/packages/ui/src/components/Header/Header.tsx @@ -34,7 +34,7 @@ export const Header = (props: HeaderProps) => { const { classes, title, tabs, customActions, breadcrumbs } = ownProps; return ( - +
{breadcrumbs && From 9318146af7e277a46ee3fefd92a4b08b35ffef9c Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sat, 14 Mar 2026 10:32:58 +0000 Subject: [PATCH 096/188] Update rename-header-css-classes.md Signed-off-by: Charles de Dreuille --- .changeset/rename-header-css-classes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/rename-header-css-classes.md b/.changeset/rename-header-css-classes.md index 3e57717c8e..664e4d7118 100644 --- a/.changeset/rename-header-css-classes.md +++ b/.changeset/rename-header-css-classes.md @@ -4,7 +4,7 @@ **BREAKING**: Renamed internal CSS classes to match the `Header` component name. -**Migration notes**: If you are targeting these classes directly in your styles, update the following: +**Migration:**: If you are targeting these classes directly in your styles, update the following: - `bui-HeaderPage` → `bui-Header` - `bui-HeaderPageContent` → `bui-HeaderContent` @@ -12,4 +12,4 @@ - `bui-HeaderPageTabsWrapper` → `bui-HeaderTabsWrapper` - `bui-HeaderPageControls` → `bui-HeaderControls` -**Affected components**: Header +**Affected components:**: Header From 612c217c1f3de9ec558767a8676c285880499987 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sat, 14 Mar 2026 10:00:41 +0000 Subject: [PATCH 097/188] fix(ui): open external Table row hrefs in a new tab Automatically apply `target="_blank"` and `rel="noopener noreferrer"` on `Row` when `href` is an external link, so external links in default BUI Table rows open in a new tab instead of navigating the current page. `rel` tokens are merged rather than replaced, so consumer-provided tokens (e.g. `rel="nofollow"`) are preserved while `noopener noreferrer` is always enforced whenever `target="_blank"` is in effect. Signed-off-by: Charles de Dreuille --- .changeset/fix-table-row-external-href.md | 7 ++++++ .../src/components/Table/components/Row.tsx | 23 +++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-table-row-external-href.md diff --git a/.changeset/fix-table-row-external-href.md b/.changeset/fix-table-row-external-href.md new file mode 100644 index 0000000000..c3e5b1f494 --- /dev/null +++ b/.changeset/fix-table-row-external-href.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Fixed `Table` rows with external `href` values to open in a new tab by automatically applying `target="_blank"` and `rel="noopener noreferrer"`. + +**Affected components**: Table diff --git a/packages/ui/src/components/Table/components/Row.tsx b/packages/ui/src/components/Table/components/Row.tsx index 3da46b3eaa..34b38c784e 100644 --- a/packages/ui/src/components/Table/components/Row.tsx +++ b/packages/ui/src/components/Table/components/Row.tsx @@ -36,9 +36,26 @@ export function Row(props: RowProps) { props, ); const { classes, columns, children, href } = ownProps; - const hasInternalHref = !!href && !isExternalLink(href); + const isExternal = isExternalLink(href); + const hasInternalHref = !!href && !isExternal; + const hasExternalHref = !!href && isExternal; const hasInteraction = !!restProps.onAction || !!href; + // Derive the effective target, defaulting to _blank for external links. + const effectiveTarget = hasExternalHref ? '_blank' : restProps.target; + // Always include noopener noreferrer when target=_blank, merging any + // consumer-provided rel tokens to avoid reverse-tabnabbing risk. + const effectiveRel = + effectiveTarget === '_blank' + ? [ + ...new Set([ + 'noopener', + 'noreferrer', + ...(restProps.rel?.split(/\s+/).filter(Boolean) ?? []), + ]), + ].join(' ') + : restProps.rel; + const handlePress = hasInteraction ? () => { restProps.onAction?.(); @@ -71,9 +88,11 @@ export function Row(props: RowProps) { {content} From 421641b0f5a63bc4ef020d15249a8cecb59bbc95 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sat, 14 Mar 2026 10:41:32 +0000 Subject: [PATCH 098/188] Update Header.module.css Signed-off-by: Charles de Dreuille --- packages/ui/src/components/Header/Header.module.css | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/components/Header/Header.module.css b/packages/ui/src/components/Header/Header.module.css index 7f12f5adf8..d74d3eeb50 100644 --- a/packages/ui/src/components/Header/Header.module.css +++ b/packages/ui/src/components/Header/Header.module.css @@ -22,7 +22,6 @@ flex-direction: column; gap: var(--bui-space-1); margin-top: var(--bui-space-6); - margin-bottom: var(--bui-space-6); } .bui-HeaderContent { From 0733b76b757ee9ad82ff2dae0c368d9ec20c0922 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sat, 14 Mar 2026 10:44:22 +0000 Subject: [PATCH 099/188] Update Row.tsx Signed-off-by: Charles de Dreuille --- packages/ui/src/components/Table/components/Row.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/Table/components/Row.tsx b/packages/ui/src/components/Table/components/Row.tsx index 34b38c784e..75b715f7f7 100644 --- a/packages/ui/src/components/Table/components/Row.tsx +++ b/packages/ui/src/components/Table/components/Row.tsx @@ -91,7 +91,7 @@ export function Row(props: RowProps) { {...restProps} target={effectiveTarget} rel={effectiveRel} - className={classes.root} + className={clsx(classes.root, restProps.className)} data-react-aria-pressable={hasInternalHref ? 'true' : undefined} onAction={handlePress} > From 94a885a2ef0546de5b061bed0fd44504d26c9200 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 27 Feb 2026 23:25:27 +0100 Subject: [PATCH 100/188] Add cli-plugin package role Signed-off-by: Patrik Oldsberg --- .changeset/add-cli-plugin-role.md | 5 +++++ .changeset/cli-support-cli-plugin-role.md | 5 +++++ packages/cli-node/report.api.md | 1 + packages/cli-node/src/roles/PackageRoles.ts | 5 +++++ packages/cli-node/src/roles/types.ts | 1 + packages/cli/config/eslint-factory.js | 1 + packages/cli/config/jest.js | 1 + .../src/modules/build/lib/typeDistProject.test.ts | 1 + .../src/modules/maintenance/commands/repo/fix.ts | 14 ++++++++++++-- .../src/modules/migrate/commands/packageScripts.ts | 2 +- 10 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 .changeset/add-cli-plugin-role.md create mode 100644 .changeset/cli-support-cli-plugin-role.md diff --git a/.changeset/add-cli-plugin-role.md b/.changeset/add-cli-plugin-role.md new file mode 100644 index 0000000000..b31c631762 --- /dev/null +++ b/.changeset/add-cli-plugin-role.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': patch +--- + +Added a new `cli-plugin` package role for packages that provide CLI plugin extensions. diff --git a/.changeset/cli-support-cli-plugin-role.md b/.changeset/cli-support-cli-plugin-role.md new file mode 100644 index 0000000000..d56c30db78 --- /dev/null +++ b/.changeset/cli-support-cli-plugin-role.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added support for the new `cli-plugin` package role in the build system, ESLint configuration, Jest configuration, and maintenance commands. diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 9d4887ca59..05d1545ae4 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -187,6 +187,7 @@ export type PackageRole = | 'frontend' | 'backend' | 'cli' + | 'cli-plugin' | 'web-library' | 'node-library' | 'common-library' diff --git a/packages/cli-node/src/roles/PackageRoles.ts b/packages/cli-node/src/roles/PackageRoles.ts index 70b62e2479..b0ee82af84 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-plugin', + 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..29e5fbb5d6 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-plugin' | 'web-library' | 'node-library' | 'common-library' diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js index dbb40537ab..db547f9b15 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-plugin': case 'node-library': case 'backend': case 'backend-plugin': diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index c670f0fe8b..07f92002ae 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -38,6 +38,7 @@ const FRONTEND_ROLES = [ const NODE_ROLES = [ 'backend', 'cli', + 'cli-plugin', 'node-library', 'backend-plugin', 'backend-plugin-module', diff --git a/packages/cli/src/modules/build/lib/typeDistProject.test.ts b/packages/cli/src/modules/build/lib/typeDistProject.test.ts index 6eef53816b..c773c505be 100644 --- a/packages/cli/src/modules/build/lib/typeDistProject.test.ts +++ b/packages/cli/src/modules/build/lib/typeDistProject.test.ts @@ -33,6 +33,7 @@ describe('typeDistProject', () => { frontend: false, backend: false, cli: false, + 'cli-plugin': false, 'common-library': false, }; diff --git a/packages/cli/src/modules/maintenance/commands/repo/fix.ts b/packages/cli/src/modules/maintenance/commands/repo/fix.ts index dc2e554121..324b4c6a3f 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/fix.ts +++ b/packages/cli/src/modules/maintenance/commands/repo/fix.ts @@ -294,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-plugin' + ) { return; } @@ -376,7 +381,12 @@ export function fixPluginPackages( return; } - if (role === 'backend' || role === 'frontend' || role === 'cli') { + if ( + role === 'backend' || + role === 'frontend' || + role === 'cli' || + role === 'cli-plugin' + ) { return; } diff --git a/packages/cli/src/modules/migrate/commands/packageScripts.ts b/packages/cli/src/modules/migrate/commands/packageScripts.ts index 901cd8fc82..f9b4b56591 100644 --- a/packages/cli/src/modules/migrate/commands/packageScripts.ts +++ b/packages/cli/src/modules/migrate/commands/packageScripts.ts @@ -22,7 +22,7 @@ import type { CommandContext } from '../../../wiring/types'; const configArgPattern = /--config[=\s][^\s$]+/; -const noStartRoles: PackageRole[] = ['cli', 'common-library']; +const noStartRoles: PackageRole[] = ['cli', 'cli-plugin', 'common-library']; export default async ({ args, info }: CommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); From 0be3eab18b6375b3b458c539bdd5b2d7ce23d187 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 10 Mar 2026 22:06:36 +0100 Subject: [PATCH 101/188] cli: initial cli-plugin-api Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/cli-use-cli-plugin-api.md | 5 + .changeset/create-cli-plugin-api.md | 5 + packages/cli-plugin-api/catalog-info.yaml | 10 ++ packages/cli-plugin-api/package.json | 40 ++++++++ packages/cli-plugin-api/report.api.md | 96 +++++++++++++++++++ .../cli-plugin-api/src/createCliPlugin.ts | 36 +++++++ .../src/describeParentCallSite.ts | 54 +++++++++++ packages/cli-plugin-api/src/errors.ts | 72 ++++++++++++++ .../src/index.ts} | 18 +++- packages/cli-plugin-api/src/internals.ts | 40 ++++++++ packages/cli-plugin-api/src/lazy.ts | 59 ++++++++++++ packages/cli-plugin-api/src/types.ts | 91 ++++++++++++++++++ packages/cli/package.json | 1 + packages/cli/src/modules/build/index.ts | 2 +- .../cli/src/modules/config/commands/docs.ts | 2 +- .../cli/src/modules/config/commands/print.ts | 2 +- .../cli/src/modules/config/commands/schema.ts | 2 +- .../src/modules/config/commands/validate.ts | 2 +- packages/cli/src/modules/config/index.ts | 2 +- .../src/modules/create-github-app/index.ts | 2 +- .../cli/src/modules/info/commands/info.ts | 2 +- packages/cli/src/modules/info/index.ts | 2 +- packages/cli/src/modules/lint/index.ts | 2 +- packages/cli/src/modules/maintenance/index.ts | 2 +- packages/cli/src/modules/migrate/index.ts | 2 +- packages/cli/src/modules/new/index.ts | 2 +- packages/cli/src/modules/test/index.ts | 2 +- .../modules/translations/commands/export.ts | 2 +- .../modules/translations/commands/import.ts | 2 +- .../cli/src/modules/translations/index.ts | 2 +- packages/cli/src/wiring/CliInitializer.ts | 13 ++- packages/cli/src/wiring/errors.ts | 47 +-------- packages/cli/src/wiring/factory.ts | 16 +--- packages/cli/src/wiring/lazy.ts | 32 +------ packages/cli/src/wiring/types.ts | 57 ++--------- yarn.lock | 11 +++ 36 files changed, 569 insertions(+), 168 deletions(-) create mode 100644 .changeset/cli-use-cli-plugin-api.md create mode 100644 .changeset/create-cli-plugin-api.md create mode 100644 packages/cli-plugin-api/catalog-info.yaml create mode 100644 packages/cli-plugin-api/package.json create mode 100644 packages/cli-plugin-api/report.api.md create mode 100644 packages/cli-plugin-api/src/createCliPlugin.ts create mode 100644 packages/cli-plugin-api/src/describeParentCallSite.ts create mode 100644 packages/cli-plugin-api/src/errors.ts rename packages/{cli/src/wiring/describeParentCallSite.ts => cli-plugin-api/src/index.ts} (58%) create mode 100644 packages/cli-plugin-api/src/internals.ts create mode 100644 packages/cli-plugin-api/src/lazy.ts create mode 100644 packages/cli-plugin-api/src/types.ts diff --git a/.changeset/cli-use-cli-plugin-api.md b/.changeset/cli-use-cli-plugin-api.md new file mode 100644 index 0000000000..c16d0e2c06 --- /dev/null +++ b/.changeset/cli-use-cli-plugin-api.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Migrated CLI plugin modules to import from the new `@backstage/cli-plugin-api` package. diff --git a/.changeset/create-cli-plugin-api.md b/.changeset/create-cli-plugin-api.md new file mode 100644 index 0000000000..0b758c76dd --- /dev/null +++ b/.changeset/create-cli-plugin-api.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-plugin-api': minor +--- + +Added a new `@backstage/cli-plugin-api` package that provides the public API for building Backstage CLI plugins. This includes `createCliPlugin`, `lazy`, `ExitCodeError`, `exitWithError`, and associated types. diff --git a/packages/cli-plugin-api/catalog-info.yaml b/packages/cli-plugin-api/catalog-info.yaml new file mode 100644 index 0000000000..3a0aca14b9 --- /dev/null +++ b/packages/cli-plugin-api/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-plugin-api + title: '@backstage/cli-plugin-api' + description: API for building Backstage CLI plugins +spec: + lifecycle: experimental + type: backstage-cli-plugin + owner: tooling-maintainers diff --git a/packages/cli-plugin-api/package.json b/packages/cli-plugin-api/package.json new file mode 100644 index 0000000000..d7fb328943 --- /dev/null +++ b/packages/cli-plugin-api/package.json @@ -0,0 +1,40 @@ +{ + "name": "@backstage/cli-plugin-api", + "version": "0.1.0", + "description": "API for building Backstage CLI plugins", + "backstage": { + "role": "cli-plugin" + }, + "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-plugin-api" + }, + "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", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/errors": "workspace:^", + "chalk": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/packages/cli-plugin-api/report.api.md b/packages/cli-plugin-api/report.api.md new file mode 100644 index 0000000000..7d5b9ed63c --- /dev/null +++ b/packages/cli-plugin-api/report.api.md @@ -0,0 +1,96 @@ +## API Report File for "@backstage/cli-plugin-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CustomErrorBase } from '@backstage/errors'; + +// @public +export type ActionExports = { + [KName in keyof TModule as TModule[KName] extends ( + ...args: any[] + ) => Promise + ? KName + : never]: TModule[KName]; +}; + +// @public +export interface BackstageCommand { + // (undocumented) + deprecated?: boolean; + // (undocumented) + description: string; + // (undocumented) + execute: + | CommandExecuteFn + | { + loader: () => Promise<{ + default: CommandExecuteFn; + }>; + }; + // (undocumented) + path: string[]; +} + +// @public +export type CliFeature = CliPlugin; + +// @public +export interface CliPlugin { + // (undocumented) + readonly $$type: '@backstage/CliPlugin'; + // (undocumented) + readonly pluginId: string; +} + +// @public +export interface CommandContext { + // (undocumented) + args: string[]; + // (undocumented) + info: { + usage: string; + description: string; + }; +} + +// @public +export type CommandExecuteFn = (context: CommandContext) => Promise; + +// @public +export function createCliPlugin(options: { + pluginId: string; + init: (registry: { + addCommand: (command: BackstageCommand) => void; + }) => Promise; +}): CliPlugin; + +// @public +export class ExitCodeError extends CustomErrorBase { + constructor(code: number, command?: string); + // (undocumented) + readonly code: number; +} + +// @public +export function exitWithError(error: unknown): never; + +// @public +export function initializeCliPlugin( + plugin: CliPlugin, + registry: { + addCommand: (command: BackstageCommand) => void; + }, +): Promise; + +// @public +export function isCliPlugin(value: unknown): value is CliPlugin; + +// @public +export function lazy( + moduleLoader: () => Promise, + exportName: keyof ActionExports, +): (...args: any[]) => Promise; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-plugin-api/src/createCliPlugin.ts b/packages/cli-plugin-api/src/createCliPlugin.ts new file mode 100644 index 0000000000..df2d3e02fd --- /dev/null +++ b/packages/cli-plugin-api/src/createCliPlugin.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 { describeParentCallSite } from './describeParentCallSite'; +import { BackstageCommand, CliPlugin, OpaqueCliPlugin } from './types'; + +/** + * Creates a new CLI plugin that provides commands to the Backstage CLI. + * + * @public + */ +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()}'`, + }); +} diff --git a/packages/cli-plugin-api/src/describeParentCallSite.ts b/packages/cli-plugin-api/src/describeParentCallSite.ts new file mode 100644 index 0000000000..641365bde8 --- /dev/null +++ b/packages/cli-plugin-api/src/describeParentCallSite.ts @@ -0,0 +1,54 @@ +/* + * 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. + */ + +const MESSAGE_MARKER = 'eHgtF5hmbrXyiEvo'; + +/** + * Internal helper that describes the location of the parent caller. + * @internal + */ +export function describeParentCallSite( + ErrorConstructor: { new (message: string): Error } = Error, +): string { + const { stack } = new ErrorConstructor(MESSAGE_MARKER); + if (!stack) { + return ''; + } + + const startIndex = stack.includes(MESSAGE_MARKER) + ? stack.indexOf('\n') + 1 + : 0; + const secondEntryStart = + stack.indexOf('\n', stack.indexOf('\n', startIndex) + 1) + 1; + const secondEntryEnd = stack.indexOf('\n', secondEntryStart); + + const line = stack.substring(secondEntryStart, secondEntryEnd).trim(); + if (!line) { + return 'unknown'; + } + + // Chrome + if (line.includes('(')) { + return line.substring(line.indexOf('(') + 1, line.indexOf(')')); + } + + // Safari & Firefox + if (line.includes('@')) { + return line.substring(line.indexOf('@') + 1); + } + + return line; +} diff --git a/packages/cli-plugin-api/src/errors.ts b/packages/cli-plugin-api/src/errors.ts new file mode 100644 index 0000000000..5334345608 --- /dev/null +++ b/packages/cli-plugin-api/src/errors.ts @@ -0,0 +1,72 @@ +/* + * 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 { CustomErrorBase, isError, stringifyError } from '@backstage/errors'; +import chalk from 'chalk'; + +/** + * An error that indicates a child process exited with a specific code. + * + * @public + */ +export class ExitCodeError extends CustomErrorBase { + readonly code: number; + + constructor(code: number, command?: string) { + super( + command + ? `Command '${command}' exited with code ${code}` + : `Child exited with code ${code}`, + ); + this.code = code; + } +} + +function exit(message: string, code: number = 1): never { + process.stderr.write(`\n${chalk.red(message)}\n\n`); + process.exit(code); +} + +/** + * Exits the process with an appropriate error code based on the error type. + * + * @public + */ +export function exitWithError(error: unknown): never { + if (!isError(error)) { + process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); + process.exit(1); + } + + switch (error.name) { + case 'InputError': + return exit(error.message, 74 /* input/output error */); + case 'NotFoundError': + return exit(error.message, 127 /* command not found */); + case 'NotImplementedError': + return exit(error.message, 64 /* command line usage error */); + case 'AuthenticationError': + case 'NotAllowedError': + return exit(error.message, 77 /* permission denied */); + case 'ExitCodeError': + return exit( + error.message, + 'code' in error && typeof error.code === 'number' ? error.code : 1, + ); + default: + return exit(stringifyError(error), 1); + } +} diff --git a/packages/cli/src/wiring/describeParentCallSite.ts b/packages/cli-plugin-api/src/index.ts similarity index 58% rename from packages/cli/src/wiring/describeParentCallSite.ts rename to packages/cli-plugin-api/src/index.ts index 35603e33b0..be990b7357 100644 --- a/packages/cli/src/wiring/describeParentCallSite.ts +++ b/packages/cli-plugin-api/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 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,7 +14,15 @@ * 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'; +export { createCliPlugin } from './createCliPlugin'; +export { lazy } from './lazy'; +export type { ActionExports } from './lazy'; +export { ExitCodeError, exitWithError } from './errors'; +export type { + BackstageCommand, + CommandContext, + CommandExecuteFn, + CliPlugin, + CliFeature, +} from './types'; +export { isCliPlugin, initializeCliPlugin } from './internals'; diff --git a/packages/cli-plugin-api/src/internals.ts b/packages/cli-plugin-api/src/internals.ts new file mode 100644 index 0000000000..99c1c4d1f8 --- /dev/null +++ b/packages/cli-plugin-api/src/internals.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 { BackstageCommand, CliPlugin, OpaqueCliPlugin } from './types'; + +/** + * Checks whether a value is a {@link CliPlugin}. + * + * @public + */ +export function isCliPlugin(value: unknown): value is CliPlugin { + return OpaqueCliPlugin.isType(value); +} + +/** + * Initializes a CLI plugin by calling its init function with the provided + * command registry. + * + * @public + */ +export async function initializeCliPlugin( + plugin: CliPlugin, + registry: { addCommand: (command: BackstageCommand) => void }, +): Promise { + const internal = OpaqueCliPlugin.toInternal(plugin); + await internal.init(registry); +} diff --git a/packages/cli-plugin-api/src/lazy.ts b/packages/cli-plugin-api/src/lazy.ts new file mode 100644 index 0000000000..30b8568fb1 --- /dev/null +++ b/packages/cli-plugin-api/src/lazy.ts @@ -0,0 +1,59 @@ +/* + * 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 { assertError } from '@backstage/errors'; +import { exitWithError } from './errors'; + +/** + * A mapped type that extracts keys of a module whose values are async action functions. + * + * @public + */ +export type ActionExports = { + [KName in keyof TModule as TModule[KName] extends ( + ...args: any[] + ) => Promise + ? KName + : never]: TModule[KName]; +}; + +/** + * Wraps an action function so that it always exits and handles errors. + * + * @public + */ +export function lazy( + moduleLoader: () => Promise, + exportName: keyof ActionExports, +): (...args: any[]) => Promise { + return async (...args: any[]) => { + try { + const mod = await moduleLoader(); + const actualModule = ( + mod as unknown as { default: ActionExports } + ).default; + const actionFunc = actualModule[exportName] as ( + ...args: any[] + ) => Promise; + await actionFunc(...args); + + process.exit(0); + } catch (error) { + assertError(error); + exitWithError(error); + } + }; +} diff --git a/packages/cli-plugin-api/src/types.ts b/packages/cli-plugin-api/src/types.ts new file mode 100644 index 0000000000..1736537580 --- /dev/null +++ b/packages/cli-plugin-api/src/types.ts @@ -0,0 +1,91 @@ +/* + * 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 { OpaqueType } from '@internal/opaque'; + +/** + * The context provided to a CLI command when it is executed. + * + * @public + */ +export interface CommandContext { + 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; + }; +} + +/** + * A function that executes a CLI command. + * + * @public + */ +export type CommandExecuteFn = (context: CommandContext) => Promise; + +/** + * A command definition for a Backstage CLI plugin. + * + * @public + */ +export interface BackstageCommand { + path: string[]; + description: string; + deprecated?: boolean; + experimental?: boolean; + execute: + | CommandExecuteFn + | { + loader: () => Promise<{ default: CommandExecuteFn }>; + }; +} + +/** + * A CLI feature, currently always a CLI plugin. + * + * @public + */ +export type CliFeature = CliPlugin; + +/** + * A Backstage CLI plugin. + * + * @public + */ +export interface CliPlugin { + readonly pluginId: string; + readonly $$type: '@backstage/CliPlugin'; +} + +/** @internal */ +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'], +}); diff --git a/packages/cli/package.json b/packages/cli/package.json index 8e03c60d0f..d309e0453c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,6 +50,7 @@ "@backstage/catalog-model": "workspace:^", "@backstage/cli-common": "workspace:^", "@backstage/cli-node": "workspace:^", + "@backstage/cli-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/config-loader": "workspace:^", "@backstage/errors": "workspace:^", diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index 139c591af2..2ccf1e3b6e 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export const buildPlugin = createCliPlugin({ pluginId: 'build', diff --git a/packages/cli/src/modules/config/commands/docs.ts b/packages/cli/src/modules/config/commands/docs.ts index 84d2f384e1..8015947a57 100644 --- a/packages/cli/src/modules/config/commands/docs.ts +++ b/packages/cli/src/modules/config/commands/docs.ts @@ -21,7 +21,7 @@ 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 { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; const DOCS_URL = 'https://config.backstage.io'; diff --git a/packages/cli/src/modules/config/commands/print.ts b/packages/cli/src/modules/config/commands/print.ts index 33f7245604..db681f4619 100644 --- a/packages/cli/src/modules/config/commands/print.ts +++ b/packages/cli/src/modules/config/commands/print.ts @@ -19,7 +19,7 @@ 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 { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/config/commands/schema.ts b/packages/cli/src/modules/config/commands/schema.ts index 13f651534c..1635caf7e6 100644 --- a/packages/cli/src/modules/config/commands/schema.ts +++ b/packages/cli/src/modules/config/commands/schema.ts @@ -20,7 +20,7 @@ import { stringify as stringifyYaml } from 'yaml'; import { loadCliConfig } from '../lib/config'; import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; -import type { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/config/commands/validate.ts b/packages/cli/src/modules/config/commands/validate.ts index f6cf9cc662..302c0b6480 100644 --- a/packages/cli/src/modules/config/commands/validate.ts +++ b/packages/cli/src/modules/config/commands/validate.ts @@ -16,7 +16,7 @@ import { cli } from 'cleye'; import { loadCliConfig } from '../lib/config'; -import type { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/config/index.ts b/packages/cli/src/modules/config/index.ts index 0d90037f18..c8fb6bca7e 100644 --- a/packages/cli/src/modules/config/index.ts +++ b/packages/cli/src/modules/config/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export const configOption = [ '--config ', diff --git a/packages/cli/src/modules/create-github-app/index.ts b/packages/cli/src/modules/create-github-app/index.ts index 9dad46577e..ea6cbf55be 100644 --- a/packages/cli/src/modules/create-github-app/index.ts +++ b/packages/cli/src/modules/create-github-app/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'new', diff --git a/packages/cli/src/modules/info/commands/info.ts b/packages/cli/src/modules/info/commands/info.ts index ab26714fca..0170c4204a 100644 --- a/packages/cli/src/modules/info/commands/info.ts +++ b/packages/cli/src/modules/info/commands/info.ts @@ -25,7 +25,7 @@ import { } from '@backstage/cli-node'; import { minimatch } from 'minimatch'; import fs from 'fs-extra'; -import type { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; /** * Attempts to read package.json from node_modules for a given package name. diff --git a/packages/cli/src/modules/info/index.ts b/packages/cli/src/modules/info/index.ts index 8e1fe9cbff..11ff207281 100644 --- a/packages/cli/src/modules/info/index.ts +++ b/packages/cli/src/modules/info/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'info', diff --git a/packages/cli/src/modules/lint/index.ts b/packages/cli/src/modules/lint/index.ts index 8e02b7653f..d825740b9f 100644 --- a/packages/cli/src/modules/lint/index.ts +++ b/packages/cli/src/modules/lint/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'lint', diff --git a/packages/cli/src/modules/maintenance/index.ts b/packages/cli/src/modules/maintenance/index.ts index a6597559f8..4356c4654f 100644 --- a/packages/cli/src/modules/maintenance/index.ts +++ b/packages/cli/src/modules/maintenance/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'maintenance', diff --git a/packages/cli/src/modules/migrate/index.ts b/packages/cli/src/modules/migrate/index.ts index c731479a51..87c0543e90 100644 --- a/packages/cli/src/modules/migrate/index.ts +++ b/packages/cli/src/modules/migrate/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'migrate', diff --git a/packages/cli/src/modules/new/index.ts b/packages/cli/src/modules/new/index.ts index 5b2a6562ea..2d30fb45bc 100644 --- a/packages/cli/src/modules/new/index.ts +++ b/packages/cli/src/modules/new/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; import { NotImplementedError } from '@backstage/errors'; export default createCliPlugin({ diff --git a/packages/cli/src/modules/test/index.ts b/packages/cli/src/modules/test/index.ts index 627648332b..f991d23ce2 100644 --- a/packages/cli/src/modules/test/index.ts +++ b/packages/cli/src/modules/test/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'test', diff --git a/packages/cli/src/modules/translations/commands/export.ts b/packages/cli/src/modules/translations/commands/export.ts index b6471f10f4..146e9ce121 100644 --- a/packages/cli/src/modules/translations/commands/export.ts +++ b/packages/cli/src/modules/translations/commands/export.ts @@ -33,7 +33,7 @@ import { formatMessagePath, validatePattern, } from '../lib/messageFilePath'; -import type { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/translations/commands/import.ts b/packages/cli/src/modules/translations/commands/import.ts index 155724dd8c..2739dbac41 100644 --- a/packages/cli/src/modules/translations/commands/import.ts +++ b/packages/cli/src/modules/translations/commands/import.ts @@ -28,7 +28,7 @@ import { createMessagePathParser, formatMessagePath, } from '../lib/messageFilePath'; -import type { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; interface ManifestRefEntry { package: string; diff --git a/packages/cli/src/modules/translations/index.ts b/packages/cli/src/modules/translations/index.ts index 43263831d5..9a001bab23 100644 --- a/packages/cli/src/modules/translations/index.ts +++ b/packages/cli/src/modules/translations/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'translations', diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index d9af1e8a4d..b44dba862f 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -15,12 +15,16 @@ */ import { CommandGraph } from './CommandGraph'; -import { BackstageCommand, CliFeature, OpaqueCliPlugin } from './types'; +import { + isCliPlugin, + initializeCliPlugin, + exitWithError, +} from '@backstage/cli-plugin-api'; +import type { BackstageCommand, CliFeature } from '@backstage/cli-plugin-api'; import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; import { version } from './version'; import chalk from 'chalk'; -import { exitWithError } from './errors'; import { ForwardedError } from '@backstage/errors'; import { isPromise } from 'node:util/types'; @@ -53,9 +57,8 @@ export class CliInitializer { } async #register(feature: CliFeature) { - if (OpaqueCliPlugin.isType(feature)) { - const internal = OpaqueCliPlugin.toInternal(feature); - await internal.init(this.commandRegistry); + if (isCliPlugin(feature)) { + await initializeCliPlugin(feature, this.commandRegistry); } else { throw new Error(`Unsupported feature type: ${(feature as any).$$type}`); } diff --git a/packages/cli/src/wiring/errors.ts b/packages/cli/src/wiring/errors.ts index aee6d3f575..b684f5c64a 100644 --- a/packages/cli/src/wiring/errors.ts +++ b/packages/cli/src/wiring/errors.ts @@ -14,49 +14,4 @@ * limitations under the License. */ -import { CustomErrorBase, isError, stringifyError } from '@backstage/errors'; -import chalk from 'chalk'; - -export class ExitCodeError extends CustomErrorBase { - readonly code: number; - - constructor(code: number, command?: string) { - super( - command - ? `Command '${command}' exited with code ${code}` - : `Child exited with code ${code}`, - ); - this.code = code; - } -} - -function exit(message: string, code: number = 1): never { - process.stderr.write(`\n${chalk.red(message)}\n\n`); - process.exit(code); -} - -export function exitWithError(error: unknown): never { - if (!isError(error)) { - process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); - process.exit(1); - } - - switch (error.name) { - case 'InputError': - return exit(error.message, 74 /* input/output error */); - case 'NotFoundError': - return exit(error.message, 127 /* command not found */); - case 'NotImplementedError': - return exit(error.message, 64 /* command line usage error */); - case 'AuthenticationError': - case 'NotAllowedError': - return exit(error.message, 77 /* permissino denied */); - case 'ExitCodeError': - return exit( - error.message, - 'code' in error && typeof error.code === 'number' ? error.code : 1, - ); - default: - return exit(stringifyError(error), 1); - } -} +export { ExitCodeError, exitWithError } from '@backstage/cli-plugin-api'; diff --git a/packages/cli/src/wiring/factory.ts b/packages/cli/src/wiring/factory.ts index 6374f37c14..19c68e805f 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 { createCliPlugin } from '@backstage/cli-plugin-api'; diff --git a/packages/cli/src/wiring/lazy.ts b/packages/cli/src/wiring/lazy.ts index 64a4d43ef4..feda134d07 100644 --- a/packages/cli/src/wiring/lazy.ts +++ b/packages/cli/src/wiring/lazy.ts @@ -14,34 +14,4 @@ * limitations under the License. */ -import { assertError } from '@backstage/errors'; -import { exitWithError } from './errors'; - -type ActionFunc = (...args: any[]) => Promise; -type ActionExports = { - [KName in keyof TModule as TModule[KName] extends ActionFunc - ? KName - : never]: TModule[KName]; -}; - -// Wraps an action function so that it always exits and handles errors -export function lazy( - moduleLoader: () => Promise, - exportName: keyof ActionExports, -): (...args: any[]) => Promise { - return async (...args: any[]) => { - try { - const mod = await moduleLoader(); - const actualModule = ( - mod as unknown as { default: ActionExports } - ).default; - const actionFunc = actualModule[exportName] as ActionFunc; - await actionFunc(...args); - - process.exit(0); - } catch (error) { - assertError(error); - exitWithError(error); - } - }; -} +export { lazy } from '@backstage/cli-plugin-api'; diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index fa3d2341f0..0cf4f849a9 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -13,53 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { OpaqueType } from '@internal/opaque'; -export interface CommandContext { - 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; - }; -} - -export type CommandExecuteFn = (context: CommandContext) => Promise; - -export interface BackstageCommand { - path: string[]; - description: string; - deprecated?: boolean; - experimental?: boolean; - execute: - | CommandExecuteFn - | { - loader: () => Promise<{ default: CommandExecuteFn }>; - }; -} - -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'], -}); +// Re-export types from the plugin API for internal use within the CLI package. +export type { + CommandContext, + CommandExecuteFn, + BackstageCommand, + CliFeature, + CliPlugin, +} from '@backstage/cli-plugin-api'; diff --git a/yarn.lock b/yarn.lock index 32b09cc5e3..6977b9161f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2822,6 +2822,16 @@ __metadata: languageName: unknown linkType: soft +"@backstage/cli-plugin-api@workspace:^, @backstage/cli-plugin-api@workspace:packages/cli-plugin-api": + version: 0.0.0-use.local + resolution: "@backstage/cli-plugin-api@workspace:packages/cli-plugin-api" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/errors": "workspace:^" + chalk: "npm:^4.0.0" + languageName: unknown + linkType: soft + "@backstage/cli@workspace:*, @backstage/cli@workspace:^, @backstage/cli@workspace:packages/cli": version: 0.0.0-use.local resolution: "@backstage/cli@workspace:packages/cli" @@ -2832,6 +2842,7 @@ __metadata: "@backstage/catalog-model": "workspace:^" "@backstage/cli-common": "workspace:^" "@backstage/cli-node": "workspace:^" + "@backstage/cli-plugin-api": "workspace:^" "@backstage/config": "workspace:^" "@backstage/config-loader": "workspace:^" "@backstage/core-app-api": "workspace:^" From d6caf7f13bebd4d90028104446d7f97b62ed5078 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 11 Mar 2026 08:09:32 +0100 Subject: [PATCH 102/188] Slim cli-plugin-api to only export createCliPlugin Move error handling and lazy utilities back to @backstage/cli since they are host-only concerns. The cli-plugin-api internals subpath provides isCliPlugin and initializeCliPlugin for the CLI host. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/cli-use-cli-plugin-api.md | 2 +- .changeset/create-cli-plugin-api.md | 2 +- packages/cli-plugin-api/.eslintrc.js | 1 + packages/cli-plugin-api/package.json | 22 ++++-- .../cli-plugin-api/report-internals.api.md | 60 ++++++++++++++++ packages/cli-plugin-api/report.api.md | 40 +---------- packages/cli-plugin-api/src/errors.ts | 72 ------------------- packages/cli-plugin-api/src/index.ts | 4 -- packages/cli-plugin-api/src/internals.ts | 7 ++ packages/cli-plugin-api/src/lazy.ts | 59 --------------- .../cli/src/modules/config/commands/docs.ts | 2 +- .../cli/src/modules/config/commands/print.ts | 2 +- .../cli/src/modules/config/commands/schema.ts | 2 +- .../src/modules/config/commands/validate.ts | 2 +- .../cli/src/modules/info/commands/info.ts | 2 +- .../modules/translations/commands/export.ts | 2 +- .../modules/translations/commands/import.ts | 2 +- packages/cli/src/wiring/CliInitializer.ts | 4 +- packages/cli/src/wiring/errors.ts | 47 +++++++++++- packages/cli/src/wiring/lazy.ts | 32 ++++++++- packages/cli/src/wiring/types.ts | 1 - yarn.lock | 1 - 22 files changed, 175 insertions(+), 193 deletions(-) create mode 100644 packages/cli-plugin-api/.eslintrc.js create mode 100644 packages/cli-plugin-api/report-internals.api.md delete mode 100644 packages/cli-plugin-api/src/errors.ts delete mode 100644 packages/cli-plugin-api/src/lazy.ts diff --git a/.changeset/cli-use-cli-plugin-api.md b/.changeset/cli-use-cli-plugin-api.md index c16d0e2c06..b55858ba62 100644 --- a/.changeset/cli-use-cli-plugin-api.md +++ b/.changeset/cli-use-cli-plugin-api.md @@ -2,4 +2,4 @@ '@backstage/cli': patch --- -Migrated CLI plugin modules to import from the new `@backstage/cli-plugin-api` package. +Migrated CLI plugin modules to use `createCliPlugin` from the new `@backstage/cli-plugin-api` package. diff --git a/.changeset/create-cli-plugin-api.md b/.changeset/create-cli-plugin-api.md index 0b758c76dd..d740fe7897 100644 --- a/.changeset/create-cli-plugin-api.md +++ b/.changeset/create-cli-plugin-api.md @@ -2,4 +2,4 @@ '@backstage/cli-plugin-api': minor --- -Added a new `@backstage/cli-plugin-api` package that provides the public API for building Backstage CLI plugins. This includes `createCliPlugin`, `lazy`, `ExitCodeError`, `exitWithError`, and associated types. +Added a new `@backstage/cli-plugin-api` package that provides the `createCliPlugin` API for building Backstage CLI plugins. diff --git a/packages/cli-plugin-api/.eslintrc.js b/packages/cli-plugin-api/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-plugin-api/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-plugin-api/package.json b/packages/cli-plugin-api/package.json index d7fb328943..051b764c6a 100644 --- a/packages/cli-plugin-api/package.json +++ b/packages/cli-plugin-api/package.json @@ -6,9 +6,7 @@ "role": "cli-plugin" }, "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" }, "homepage": "https://backstage.io", "repository": { @@ -17,8 +15,23 @@ "directory": "packages/cli-plugin-api" }, "license": "Apache-2.0", + "exports": { + ".": "./src/index.ts", + "./internals": "./src/internals.ts", + "./package.json": "./package.json" + }, "main": "src/index.ts", "types": "src/index.ts", + "typesVersions": { + "*": { + "internals": [ + "src/internals.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "files": [ "dist" ], @@ -31,8 +44,7 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/errors": "workspace:^", - "chalk": "^4.0.0" + "@backstage/errors": "workspace:^" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/packages/cli-plugin-api/report-internals.api.md b/packages/cli-plugin-api/report-internals.api.md new file mode 100644 index 0000000000..b5721e6972 --- /dev/null +++ b/packages/cli-plugin-api/report-internals.api.md @@ -0,0 +1,60 @@ +## API Report File for "@backstage/cli-plugin-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public +export interface BackstageCommand { + // (undocumented) + deprecated?: boolean; + // (undocumented) + description: string; + // (undocumented) + execute: + | CommandExecuteFn + | { + loader: () => Promise<{ + default: CommandExecuteFn; + }>; + }; + // (undocumented) + experimental?: boolean; + // (undocumented) + path: string[]; +} + +// @public +export interface CliPlugin { + // (undocumented) + readonly $$type: '@backstage/CliPlugin'; + // (undocumented) + readonly pluginId: string; +} + +// @public +export interface CommandContext { + // (undocumented) + args: string[]; + // (undocumented) + info: { + usage: string; + description: string; + }; +} + +// @public +export type CommandExecuteFn = (context: CommandContext) => Promise; + +// @public +export function initializeCliPlugin( + plugin: CliPlugin, + registry: { + addCommand: (command: BackstageCommand) => void; + }, +): Promise; + +// @public +export function isCliPlugin(value: unknown): value is CliPlugin; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-plugin-api/report.api.md b/packages/cli-plugin-api/report.api.md index 7d5b9ed63c..3562bb20e6 100644 --- a/packages/cli-plugin-api/report.api.md +++ b/packages/cli-plugin-api/report.api.md @@ -3,17 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { CustomErrorBase } from '@backstage/errors'; - -// @public -export type ActionExports = { - [KName in keyof TModule as TModule[KName] extends ( - ...args: any[] - ) => Promise - ? KName - : never]: TModule[KName]; -}; - // @public export interface BackstageCommand { // (undocumented) @@ -29,6 +18,8 @@ export interface BackstageCommand { }>; }; // (undocumented) + experimental?: boolean; + // (undocumented) path: string[]; } @@ -65,32 +56,5 @@ export function createCliPlugin(options: { }) => Promise; }): CliPlugin; -// @public -export class ExitCodeError extends CustomErrorBase { - constructor(code: number, command?: string); - // (undocumented) - readonly code: number; -} - -// @public -export function exitWithError(error: unknown): never; - -// @public -export function initializeCliPlugin( - plugin: CliPlugin, - registry: { - addCommand: (command: BackstageCommand) => void; - }, -): Promise; - -// @public -export function isCliPlugin(value: unknown): value is CliPlugin; - -// @public -export function lazy( - moduleLoader: () => Promise, - exportName: keyof ActionExports, -): (...args: any[]) => Promise; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/cli-plugin-api/src/errors.ts b/packages/cli-plugin-api/src/errors.ts deleted file mode 100644 index 5334345608..0000000000 --- a/packages/cli-plugin-api/src/errors.ts +++ /dev/null @@ -1,72 +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 { CustomErrorBase, isError, stringifyError } from '@backstage/errors'; -import chalk from 'chalk'; - -/** - * An error that indicates a child process exited with a specific code. - * - * @public - */ -export class ExitCodeError extends CustomErrorBase { - readonly code: number; - - constructor(code: number, command?: string) { - super( - command - ? `Command '${command}' exited with code ${code}` - : `Child exited with code ${code}`, - ); - this.code = code; - } -} - -function exit(message: string, code: number = 1): never { - process.stderr.write(`\n${chalk.red(message)}\n\n`); - process.exit(code); -} - -/** - * Exits the process with an appropriate error code based on the error type. - * - * @public - */ -export function exitWithError(error: unknown): never { - if (!isError(error)) { - process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); - process.exit(1); - } - - switch (error.name) { - case 'InputError': - return exit(error.message, 74 /* input/output error */); - case 'NotFoundError': - return exit(error.message, 127 /* command not found */); - case 'NotImplementedError': - return exit(error.message, 64 /* command line usage error */); - case 'AuthenticationError': - case 'NotAllowedError': - return exit(error.message, 77 /* permission denied */); - case 'ExitCodeError': - return exit( - error.message, - 'code' in error && typeof error.code === 'number' ? error.code : 1, - ); - default: - return exit(stringifyError(error), 1); - } -} diff --git a/packages/cli-plugin-api/src/index.ts b/packages/cli-plugin-api/src/index.ts index be990b7357..15082804ee 100644 --- a/packages/cli-plugin-api/src/index.ts +++ b/packages/cli-plugin-api/src/index.ts @@ -15,9 +15,6 @@ */ export { createCliPlugin } from './createCliPlugin'; -export { lazy } from './lazy'; -export type { ActionExports } from './lazy'; -export { ExitCodeError, exitWithError } from './errors'; export type { BackstageCommand, CommandContext, @@ -25,4 +22,3 @@ export type { CliPlugin, CliFeature, } from './types'; -export { isCliPlugin, initializeCliPlugin } from './internals'; diff --git a/packages/cli-plugin-api/src/internals.ts b/packages/cli-plugin-api/src/internals.ts index 99c1c4d1f8..f851decbad 100644 --- a/packages/cli-plugin-api/src/internals.ts +++ b/packages/cli-plugin-api/src/internals.ts @@ -16,6 +16,13 @@ import { BackstageCommand, CliPlugin, OpaqueCliPlugin } from './types'; +export type { + BackstageCommand, + CliPlugin, + CommandContext, + CommandExecuteFn, +} from './types'; + /** * Checks whether a value is a {@link CliPlugin}. * diff --git a/packages/cli-plugin-api/src/lazy.ts b/packages/cli-plugin-api/src/lazy.ts deleted file mode 100644 index 30b8568fb1..0000000000 --- a/packages/cli-plugin-api/src/lazy.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 { assertError } from '@backstage/errors'; -import { exitWithError } from './errors'; - -/** - * A mapped type that extracts keys of a module whose values are async action functions. - * - * @public - */ -export type ActionExports = { - [KName in keyof TModule as TModule[KName] extends ( - ...args: any[] - ) => Promise - ? KName - : never]: TModule[KName]; -}; - -/** - * Wraps an action function so that it always exits and handles errors. - * - * @public - */ -export function lazy( - moduleLoader: () => Promise, - exportName: keyof ActionExports, -): (...args: any[]) => Promise { - return async (...args: any[]) => { - try { - const mod = await moduleLoader(); - const actualModule = ( - mod as unknown as { default: ActionExports } - ).default; - const actionFunc = actualModule[exportName] as ( - ...args: any[] - ) => Promise; - await actionFunc(...args); - - process.exit(0); - } catch (error) { - assertError(error); - exitWithError(error); - } - }; -} diff --git a/packages/cli/src/modules/config/commands/docs.ts b/packages/cli/src/modules/config/commands/docs.ts index 8015947a57..84d2f384e1 100644 --- a/packages/cli/src/modules/config/commands/docs.ts +++ b/packages/cli/src/modules/config/commands/docs.ts @@ -21,7 +21,7 @@ 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 { CommandContext } from '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; const DOCS_URL = 'https://config.backstage.io'; diff --git a/packages/cli/src/modules/config/commands/print.ts b/packages/cli/src/modules/config/commands/print.ts index db681f4619..33f7245604 100644 --- a/packages/cli/src/modules/config/commands/print.ts +++ b/packages/cli/src/modules/config/commands/print.ts @@ -19,7 +19,7 @@ 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 { CommandContext } from '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/config/commands/schema.ts b/packages/cli/src/modules/config/commands/schema.ts index 1635caf7e6..13f651534c 100644 --- a/packages/cli/src/modules/config/commands/schema.ts +++ b/packages/cli/src/modules/config/commands/schema.ts @@ -20,7 +20,7 @@ import { stringify as stringifyYaml } from 'yaml'; import { loadCliConfig } from '../lib/config'; import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; -import type { CommandContext } from '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/config/commands/validate.ts b/packages/cli/src/modules/config/commands/validate.ts index 302c0b6480..f6cf9cc662 100644 --- a/packages/cli/src/modules/config/commands/validate.ts +++ b/packages/cli/src/modules/config/commands/validate.ts @@ -16,7 +16,7 @@ import { cli } from 'cleye'; import { loadCliConfig } from '../lib/config'; -import type { CommandContext } from '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/info/commands/info.ts b/packages/cli/src/modules/info/commands/info.ts index 0170c4204a..ab26714fca 100644 --- a/packages/cli/src/modules/info/commands/info.ts +++ b/packages/cli/src/modules/info/commands/info.ts @@ -25,7 +25,7 @@ import { } from '@backstage/cli-node'; import { minimatch } from 'minimatch'; import fs from 'fs-extra'; -import type { CommandContext } from '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; /** * Attempts to read package.json from node_modules for a given package name. diff --git a/packages/cli/src/modules/translations/commands/export.ts b/packages/cli/src/modules/translations/commands/export.ts index 146e9ce121..b6471f10f4 100644 --- a/packages/cli/src/modules/translations/commands/export.ts +++ b/packages/cli/src/modules/translations/commands/export.ts @@ -33,7 +33,7 @@ import { formatMessagePath, validatePattern, } from '../lib/messageFilePath'; -import type { CommandContext } from '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/translations/commands/import.ts b/packages/cli/src/modules/translations/commands/import.ts index 2739dbac41..155724dd8c 100644 --- a/packages/cli/src/modules/translations/commands/import.ts +++ b/packages/cli/src/modules/translations/commands/import.ts @@ -28,7 +28,7 @@ import { createMessagePathParser, formatMessagePath, } from '../lib/messageFilePath'; -import type { CommandContext } from '@backstage/cli-plugin-api'; +import type { CommandContext } from '../../../wiring/types'; interface ManifestRefEntry { package: string; diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index b44dba862f..72269f3274 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -18,13 +18,13 @@ import { CommandGraph } from './CommandGraph'; import { isCliPlugin, initializeCliPlugin, - exitWithError, -} from '@backstage/cli-plugin-api'; +} from '@backstage/cli-plugin-api/internals'; import type { BackstageCommand, CliFeature } from '@backstage/cli-plugin-api'; import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; import { version } from './version'; import chalk from 'chalk'; +import { exitWithError } from './errors'; import { ForwardedError } from '@backstage/errors'; import { isPromise } from 'node:util/types'; diff --git a/packages/cli/src/wiring/errors.ts b/packages/cli/src/wiring/errors.ts index b684f5c64a..aee6d3f575 100644 --- a/packages/cli/src/wiring/errors.ts +++ b/packages/cli/src/wiring/errors.ts @@ -14,4 +14,49 @@ * limitations under the License. */ -export { ExitCodeError, exitWithError } from '@backstage/cli-plugin-api'; +import { CustomErrorBase, isError, stringifyError } from '@backstage/errors'; +import chalk from 'chalk'; + +export class ExitCodeError extends CustomErrorBase { + readonly code: number; + + constructor(code: number, command?: string) { + super( + command + ? `Command '${command}' exited with code ${code}` + : `Child exited with code ${code}`, + ); + this.code = code; + } +} + +function exit(message: string, code: number = 1): never { + process.stderr.write(`\n${chalk.red(message)}\n\n`); + process.exit(code); +} + +export function exitWithError(error: unknown): never { + if (!isError(error)) { + process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); + process.exit(1); + } + + switch (error.name) { + case 'InputError': + return exit(error.message, 74 /* input/output error */); + case 'NotFoundError': + return exit(error.message, 127 /* command not found */); + case 'NotImplementedError': + return exit(error.message, 64 /* command line usage error */); + case 'AuthenticationError': + case 'NotAllowedError': + return exit(error.message, 77 /* permissino denied */); + case 'ExitCodeError': + return exit( + error.message, + 'code' in error && typeof error.code === 'number' ? error.code : 1, + ); + default: + return exit(stringifyError(error), 1); + } +} diff --git a/packages/cli/src/wiring/lazy.ts b/packages/cli/src/wiring/lazy.ts index feda134d07..64a4d43ef4 100644 --- a/packages/cli/src/wiring/lazy.ts +++ b/packages/cli/src/wiring/lazy.ts @@ -14,4 +14,34 @@ * limitations under the License. */ -export { lazy } from '@backstage/cli-plugin-api'; +import { assertError } from '@backstage/errors'; +import { exitWithError } from './errors'; + +type ActionFunc = (...args: any[]) => Promise; +type ActionExports = { + [KName in keyof TModule as TModule[KName] extends ActionFunc + ? KName + : never]: TModule[KName]; +}; + +// Wraps an action function so that it always exits and handles errors +export function lazy( + moduleLoader: () => Promise, + exportName: keyof ActionExports, +): (...args: any[]) => Promise { + return async (...args: any[]) => { + try { + const mod = await moduleLoader(); + const actualModule = ( + mod as unknown as { default: ActionExports } + ).default; + const actionFunc = actualModule[exportName] as ActionFunc; + await actionFunc(...args); + + process.exit(0); + } catch (error) { + assertError(error); + exitWithError(error); + } + }; +} diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index 0cf4f849a9..4fd58a5c73 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -// Re-export types from the plugin API for internal use within the CLI package. export type { CommandContext, CommandExecuteFn, diff --git a/yarn.lock b/yarn.lock index 6977b9161f..2173b5a314 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2828,7 +2828,6 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/errors": "workspace:^" - chalk: "npm:^4.0.0" languageName: unknown linkType: soft From 2e5f189cfa91f71eeed66dbfeb21cef7f693cdd7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 11 Mar 2026 08:18:55 +0100 Subject: [PATCH 103/188] Add @internal/cli package for opaque CLI types Move OpaqueCliPlugin to a new @internal/cli inline package, following the same pattern as @internal/frontend. Both cli-plugin-api and cli import it directly, and it gets bundled into their dists at build time. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-internal/.eslintrc.js | 1 + packages/cli-internal/catalog-info.yaml | 9 +++ packages/cli-internal/package.json | 30 ++++++++++ .../cli-internal/src/InternalCliPlugin.ts | 32 ++++++++++ packages/cli-internal/src/index.ts | 17 ++++++ packages/cli-plugin-api/package.json | 19 +----- .../cli-plugin-api/report-internals.api.md | 60 ------------------- .../cli-plugin-api/src/createCliPlugin.ts | 3 +- packages/cli-plugin-api/src/internals.ts | 47 --------------- packages/cli-plugin-api/src/types.ts | 16 ----- packages/cli/src/wiring/CliInitializer.ts | 10 ++-- yarn.lock | 9 +++ 12 files changed, 107 insertions(+), 146 deletions(-) create mode 100644 packages/cli-internal/.eslintrc.js create mode 100644 packages/cli-internal/catalog-info.yaml create mode 100644 packages/cli-internal/package.json create mode 100644 packages/cli-internal/src/InternalCliPlugin.ts create mode 100644 packages/cli-internal/src/index.ts delete mode 100644 packages/cli-plugin-api/report-internals.api.md delete mode 100644 packages/cli-plugin-api/src/internals.ts 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..80c68c74d3 --- /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-plugin-api": "workspace:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/packages/cli-internal/src/InternalCliPlugin.ts b/packages/cli-internal/src/InternalCliPlugin.ts new file mode 100644 index 0000000000..62bf5013c8 --- /dev/null +++ b/packages/cli-internal/src/InternalCliPlugin.ts @@ -0,0 +1,32 @@ +/* + * 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 { BackstageCommand, CliPlugin } from '@backstage/cli-plugin-api'; +import { OpaqueType } from '@internal/opaque'; + +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'], +}); diff --git a/packages/cli-internal/src/index.ts b/packages/cli-internal/src/index.ts new file mode 100644 index 0000000000..a1dc8ad24c --- /dev/null +++ b/packages/cli-internal/src/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 { OpaqueCliPlugin } from './InternalCliPlugin'; diff --git a/packages/cli-plugin-api/package.json b/packages/cli-plugin-api/package.json index 051b764c6a..4d256f3c96 100644 --- a/packages/cli-plugin-api/package.json +++ b/packages/cli-plugin-api/package.json @@ -6,7 +6,9 @@ "role": "cli-plugin" }, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", "repository": { @@ -15,23 +17,8 @@ "directory": "packages/cli-plugin-api" }, "license": "Apache-2.0", - "exports": { - ".": "./src/index.ts", - "./internals": "./src/internals.ts", - "./package.json": "./package.json" - }, "main": "src/index.ts", "types": "src/index.ts", - "typesVersions": { - "*": { - "internals": [ - "src/internals.ts" - ], - "package.json": [ - "package.json" - ] - } - }, "files": [ "dist" ], diff --git a/packages/cli-plugin-api/report-internals.api.md b/packages/cli-plugin-api/report-internals.api.md deleted file mode 100644 index b5721e6972..0000000000 --- a/packages/cli-plugin-api/report-internals.api.md +++ /dev/null @@ -1,60 +0,0 @@ -## API Report File for "@backstage/cli-plugin-api" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -// @public -export interface BackstageCommand { - // (undocumented) - deprecated?: boolean; - // (undocumented) - description: string; - // (undocumented) - execute: - | CommandExecuteFn - | { - loader: () => Promise<{ - default: CommandExecuteFn; - }>; - }; - // (undocumented) - experimental?: boolean; - // (undocumented) - path: string[]; -} - -// @public -export interface CliPlugin { - // (undocumented) - readonly $$type: '@backstage/CliPlugin'; - // (undocumented) - readonly pluginId: string; -} - -// @public -export interface CommandContext { - // (undocumented) - args: string[]; - // (undocumented) - info: { - usage: string; - description: string; - }; -} - -// @public -export type CommandExecuteFn = (context: CommandContext) => Promise; - -// @public -export function initializeCliPlugin( - plugin: CliPlugin, - registry: { - addCommand: (command: BackstageCommand) => void; - }, -): Promise; - -// @public -export function isCliPlugin(value: unknown): value is CliPlugin; - -// (No @packageDocumentation comment for this package) -``` diff --git a/packages/cli-plugin-api/src/createCliPlugin.ts b/packages/cli-plugin-api/src/createCliPlugin.ts index df2d3e02fd..7a8d299394 100644 --- a/packages/cli-plugin-api/src/createCliPlugin.ts +++ b/packages/cli-plugin-api/src/createCliPlugin.ts @@ -14,8 +14,9 @@ * limitations under the License. */ +import { OpaqueCliPlugin } from '@internal/cli'; import { describeParentCallSite } from './describeParentCallSite'; -import { BackstageCommand, CliPlugin, OpaqueCliPlugin } from './types'; +import { BackstageCommand, CliPlugin } from './types'; /** * Creates a new CLI plugin that provides commands to the Backstage CLI. diff --git a/packages/cli-plugin-api/src/internals.ts b/packages/cli-plugin-api/src/internals.ts deleted file mode 100644 index f851decbad..0000000000 --- a/packages/cli-plugin-api/src/internals.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 { BackstageCommand, CliPlugin, OpaqueCliPlugin } from './types'; - -export type { - BackstageCommand, - CliPlugin, - CommandContext, - CommandExecuteFn, -} from './types'; - -/** - * Checks whether a value is a {@link CliPlugin}. - * - * @public - */ -export function isCliPlugin(value: unknown): value is CliPlugin { - return OpaqueCliPlugin.isType(value); -} - -/** - * Initializes a CLI plugin by calling its init function with the provided - * command registry. - * - * @public - */ -export async function initializeCliPlugin( - plugin: CliPlugin, - registry: { addCommand: (command: BackstageCommand) => void }, -): Promise { - const internal = OpaqueCliPlugin.toInternal(plugin); - await internal.init(registry); -} diff --git a/packages/cli-plugin-api/src/types.ts b/packages/cli-plugin-api/src/types.ts index 1736537580..42d006ce5c 100644 --- a/packages/cli-plugin-api/src/types.ts +++ b/packages/cli-plugin-api/src/types.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { OpaqueType } from '@internal/opaque'; /** * The context provided to a CLI command when it is executed. @@ -74,18 +73,3 @@ export interface CliPlugin { readonly pluginId: string; readonly $$type: '@backstage/CliPlugin'; } - -/** @internal */ -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'], -}); diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index 72269f3274..bd31930854 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -15,10 +15,7 @@ */ import { CommandGraph } from './CommandGraph'; -import { - isCliPlugin, - initializeCliPlugin, -} from '@backstage/cli-plugin-api/internals'; +import { OpaqueCliPlugin } from '@internal/cli'; import type { BackstageCommand, CliFeature } from '@backstage/cli-plugin-api'; import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; @@ -57,8 +54,9 @@ export class CliInitializer { } async #register(feature: CliFeature) { - if (isCliPlugin(feature)) { - await initializeCliPlugin(feature, this.commandRegistry); + if (OpaqueCliPlugin.isType(feature)) { + const internal = OpaqueCliPlugin.toInternal(feature); + await internal.init(this.commandRegistry); } else { throw new Error(`Unsupported feature type: ${(feature as any).$$type}`); } diff --git a/yarn.lock b/yarn.lock index 2173b5a314..2ef9ed9764 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9680,6 +9680,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-plugin-api": "workspace:^" + languageName: unknown + linkType: soft + "@internal/frontend@workspace:packages/frontend-internal": version: 0.0.0-use.local resolution: "@internal/frontend@workspace:packages/frontend-internal" From 7e6ad01a21a9a369459c5c147a83e3b007955196 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 11 Mar 2026 08:38:58 +0100 Subject: [PATCH 104/188] Refine cli-plugin-api: use packageJson, inline types, rename context fields Replace pluginId with packageJson input for createCliPlugin, remove CommandExecuteFn type alias by inlining it, and rename CommandContext.info.description to info.name. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../cli-internal/src/InternalCliPlugin.ts | 6 +-- packages/cli-plugin-api/report.api.md | 13 ++--- .../cli-plugin-api/src/createCliPlugin.ts | 13 +++-- .../src/describeParentCallSite.ts | 54 ------------------- packages/cli-plugin-api/src/index.ts | 1 - packages/cli-plugin-api/src/types.ts | 18 +++---- packages/cli/src/modules/auth/index.ts | 3 +- packages/cli/src/modules/build/index.ts | 3 +- packages/cli/src/modules/config/index.ts | 3 +- .../src/modules/create-github-app/index.ts | 3 +- packages/cli/src/modules/info/index.ts | 3 +- packages/cli/src/modules/lint/index.ts | 3 +- packages/cli/src/modules/maintenance/index.ts | 3 +- .../migrate/commands/versions/bump.test.ts | 2 +- .../migrate/commands/versions/migrate.test.ts | 6 +-- packages/cli/src/modules/migrate/index.ts | 3 +- .../cli/src/modules/new/commands/new.test.ts | 2 +- packages/cli/src/modules/new/index.ts | 3 +- packages/cli/src/modules/test/index.ts | 3 +- .../cli/src/modules/translations/index.ts | 3 +- .../cli/src/wiring/CliInitializer.test.ts | 16 +++--- packages/cli/src/wiring/CliInitializer.ts | 6 ++- packages/cli/src/wiring/types.ts | 1 - 23 files changed, 60 insertions(+), 111 deletions(-) delete mode 100644 packages/cli-plugin-api/src/describeParentCallSite.ts diff --git a/packages/cli-internal/src/InternalCliPlugin.ts b/packages/cli-internal/src/InternalCliPlugin.ts index 62bf5013c8..426f3d23e6 100644 --- a/packages/cli-internal/src/InternalCliPlugin.ts +++ b/packages/cli-internal/src/InternalCliPlugin.ts @@ -21,10 +21,8 @@ export const OpaqueCliPlugin = OpaqueType.create<{ public: CliPlugin; versions: { readonly version: 'v1'; - readonly description: string; - init: (registry: { - addCommand: (command: BackstageCommand) => void; - }) => Promise; + readonly packageName: string; + readonly commands: Promise>; }; }>({ type: '@backstage/CliPlugin', diff --git a/packages/cli-plugin-api/report.api.md b/packages/cli-plugin-api/report.api.md index 3562bb20e6..d72169495f 100644 --- a/packages/cli-plugin-api/report.api.md +++ b/packages/cli-plugin-api/report.api.md @@ -11,10 +11,10 @@ export interface BackstageCommand { description: string; // (undocumented) execute: - | CommandExecuteFn + | ((context: CommandContext) => Promise) | { loader: () => Promise<{ - default: CommandExecuteFn; + default: (context: CommandContext) => Promise; }>; }; // (undocumented) @@ -30,8 +30,6 @@ export type CliFeature = CliPlugin; export interface CliPlugin { // (undocumented) readonly $$type: '@backstage/CliPlugin'; - // (undocumented) - readonly pluginId: string; } // @public @@ -45,12 +43,11 @@ export interface CommandContext { }; } -// @public -export type CommandExecuteFn = (context: CommandContext) => Promise; - // @public export function createCliPlugin(options: { - pluginId: string; + packageJson: { + name: string; + }; init: (registry: { addCommand: (command: BackstageCommand) => void; }) => Promise; diff --git a/packages/cli-plugin-api/src/createCliPlugin.ts b/packages/cli-plugin-api/src/createCliPlugin.ts index 7a8d299394..04a26665a6 100644 --- a/packages/cli-plugin-api/src/createCliPlugin.ts +++ b/packages/cli-plugin-api/src/createCliPlugin.ts @@ -15,7 +15,6 @@ */ import { OpaqueCliPlugin } from '@internal/cli'; -import { describeParentCallSite } from './describeParentCallSite'; import { BackstageCommand, CliPlugin } from './types'; /** @@ -24,14 +23,18 @@ import { BackstageCommand, CliPlugin } from './types'; * @public */ export function createCliPlugin(options: { - pluginId: string; + packageJson: { name: string }; init: (registry: { addCommand: (command: BackstageCommand) => void; }) => Promise; }): CliPlugin { + const commands: BackstageCommand[] = []; + const commandsPromise = options + .init({ addCommand: command => commands.push(command) }) + .then(() => commands); + return OpaqueCliPlugin.createInstance('v1', { - pluginId: options.pluginId, - init: options.init, - description: `created at '${describeParentCallSite()}'`, + packageName: options.packageJson.name, + commands: commandsPromise, }); } diff --git a/packages/cli-plugin-api/src/describeParentCallSite.ts b/packages/cli-plugin-api/src/describeParentCallSite.ts deleted file mode 100644 index 641365bde8..0000000000 --- a/packages/cli-plugin-api/src/describeParentCallSite.ts +++ /dev/null @@ -1,54 +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. - */ - -const MESSAGE_MARKER = 'eHgtF5hmbrXyiEvo'; - -/** - * Internal helper that describes the location of the parent caller. - * @internal - */ -export function describeParentCallSite( - ErrorConstructor: { new (message: string): Error } = Error, -): string { - const { stack } = new ErrorConstructor(MESSAGE_MARKER); - if (!stack) { - return ''; - } - - const startIndex = stack.includes(MESSAGE_MARKER) - ? stack.indexOf('\n') + 1 - : 0; - const secondEntryStart = - stack.indexOf('\n', stack.indexOf('\n', startIndex) + 1) + 1; - const secondEntryEnd = stack.indexOf('\n', secondEntryStart); - - const line = stack.substring(secondEntryStart, secondEntryEnd).trim(); - if (!line) { - return 'unknown'; - } - - // Chrome - if (line.includes('(')) { - return line.substring(line.indexOf('(') + 1, line.indexOf(')')); - } - - // Safari & Firefox - if (line.includes('@')) { - return line.substring(line.indexOf('@') + 1); - } - - return line; -} diff --git a/packages/cli-plugin-api/src/index.ts b/packages/cli-plugin-api/src/index.ts index 15082804ee..988600280c 100644 --- a/packages/cli-plugin-api/src/index.ts +++ b/packages/cli-plugin-api/src/index.ts @@ -18,7 +18,6 @@ export { createCliPlugin } from './createCliPlugin'; export type { BackstageCommand, CommandContext, - CommandExecuteFn, CliPlugin, CliFeature, } from './types'; diff --git a/packages/cli-plugin-api/src/types.ts b/packages/cli-plugin-api/src/types.ts index 42d006ce5c..3bdf026490 100644 --- a/packages/cli-plugin-api/src/types.ts +++ b/packages/cli-plugin-api/src/types.ts @@ -27,19 +27,12 @@ export interface CommandContext { */ usage: string; /** - * The description provided for the command + * The name of the command, for example: "repo test" */ - description: string; + name: string; }; } -/** - * A function that executes a CLI command. - * - * @public - */ -export type CommandExecuteFn = (context: CommandContext) => Promise; - /** * A command definition for a Backstage CLI plugin. * @@ -51,9 +44,11 @@ export interface BackstageCommand { deprecated?: boolean; experimental?: boolean; execute: - | CommandExecuteFn + | ((context: CommandContext) => Promise) | { - loader: () => Promise<{ default: CommandExecuteFn }>; + loader: () => Promise<{ + default: (context: CommandContext) => Promise; + }>; }; } @@ -70,6 +65,5 @@ export type CliFeature = CliPlugin; * @public */ export interface CliPlugin { - readonly pluginId: string; readonly $$type: '@backstage/CliPlugin'; } diff --git a/packages/cli/src/modules/auth/index.ts b/packages/cli/src/modules/auth/index.ts index d5766219b2..6ce849a9b9 100644 --- a/packages/cli/src/modules/auth/index.ts +++ b/packages/cli/src/modules/auth/index.ts @@ -15,9 +15,10 @@ */ import { createCliPlugin } from '../../wiring/factory'; +import packageJson from '../../../package.json'; export default createCliPlugin({ - pluginId: 'auth', + packageJson, init: async reg => { reg.addCommand({ path: ['auth', 'login'], diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index 2ccf1e3b6e..1c8d2217ed 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -15,9 +15,10 @@ */ import { createCliPlugin } from '@backstage/cli-plugin-api'; +import packageJson from '../../../package.json'; export const buildPlugin = createCliPlugin({ - pluginId: 'build', + packageJson, init: async reg => { reg.addCommand({ path: ['package', 'build'], diff --git a/packages/cli/src/modules/config/index.ts b/packages/cli/src/modules/config/index.ts index c8fb6bca7e..3c59f96948 100644 --- a/packages/cli/src/modules/config/index.ts +++ b/packages/cli/src/modules/config/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { createCliPlugin } from '@backstage/cli-plugin-api'; +import packageJson from '../../../package.json'; export const configOption = [ '--config ', @@ -23,7 +24,7 @@ export const configOption = [ ] as const; export default createCliPlugin({ - pluginId: 'config', + packageJson, init: async reg => { reg.addCommand({ path: ['config:docs'], diff --git a/packages/cli/src/modules/create-github-app/index.ts b/packages/cli/src/modules/create-github-app/index.ts index ea6cbf55be..69fe5869d4 100644 --- a/packages/cli/src/modules/create-github-app/index.ts +++ b/packages/cli/src/modules/create-github-app/index.ts @@ -14,9 +14,10 @@ * limitations under the License. */ import { createCliPlugin } from '@backstage/cli-plugin-api'; +import packageJson from '../../../package.json'; export default createCliPlugin({ - pluginId: 'new', + packageJson, init: async reg => { reg.addCommand({ path: ['create-github-app'], diff --git a/packages/cli/src/modules/info/index.ts b/packages/cli/src/modules/info/index.ts index 11ff207281..df8a848f11 100644 --- a/packages/cli/src/modules/info/index.ts +++ b/packages/cli/src/modules/info/index.ts @@ -14,9 +14,10 @@ * limitations under the License. */ import { createCliPlugin } from '@backstage/cli-plugin-api'; +import packageJson from '../../../package.json'; export default createCliPlugin({ - pluginId: 'info', + packageJson, init: async reg => { reg.addCommand({ path: ['info'], diff --git a/packages/cli/src/modules/lint/index.ts b/packages/cli/src/modules/lint/index.ts index d825740b9f..51e5b981a4 100644 --- a/packages/cli/src/modules/lint/index.ts +++ b/packages/cli/src/modules/lint/index.ts @@ -14,9 +14,10 @@ * limitations under the License. */ import { createCliPlugin } from '@backstage/cli-plugin-api'; +import packageJson from '../../../package.json'; export default createCliPlugin({ - pluginId: 'lint', + packageJson, init: async reg => { reg.addCommand({ path: ['package', 'lint'], diff --git a/packages/cli/src/modules/maintenance/index.ts b/packages/cli/src/modules/maintenance/index.ts index 4356c4654f..142bd543f2 100644 --- a/packages/cli/src/modules/maintenance/index.ts +++ b/packages/cli/src/modules/maintenance/index.ts @@ -14,9 +14,10 @@ * limitations under the License. */ import { createCliPlugin } from '@backstage/cli-plugin-api'; +import packageJson from '../../../package.json'; export default createCliPlugin({ - pluginId: 'maintenance', + packageJson, init: async reg => { reg.addCommand({ path: ['repo', 'fix'], diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts index f119a0bedb..c7694f5e09 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts @@ -125,7 +125,7 @@ const expectLogsToMatch = ( expect(receivedLogs.filter(Boolean).sort()).toEqual(expected.sort()); }; -const info = { usage: 'backstage-cli versions:bump', description: '' }; +const info = { usage: 'backstage-cli versions:bump', name: 'versions:bump' }; describe('bump', () => { const mockDir = createMockDirectory(); diff --git a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts b/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts index fc50a5c491..606fa528b1 100644 --- a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts @@ -123,7 +123,7 @@ describe('versions:migrate', () => { }); const { warn, log: logs } = await withLogCollector(async () => { - await migrate({ args: [], info: { usage: 'test', description: 'test' } }); + await migrate({ args: [], info: { usage: 'test', name: 'test' } }); }); expectLogsToMatch(logs, [ @@ -229,7 +229,7 @@ describe('versions:migrate', () => { }); await withLogCollector(async () => { - await migrate({ args: [], info: { usage: 'test', description: 'test' } }); + await migrate({ args: [], info: { usage: 'test', name: 'test' } }); }); expect(runObj.run).toHaveBeenCalledTimes(1); @@ -311,7 +311,7 @@ describe('versions:migrate', () => { }); await withLogCollector(async () => { - await migrate({ args: [], info: { usage: 'test', description: 'test' } }); + await migrate({ args: [], info: { usage: 'test', name: 'test' } }); }); expect(runObj.run).toHaveBeenCalledTimes(1); diff --git a/packages/cli/src/modules/migrate/index.ts b/packages/cli/src/modules/migrate/index.ts index 87c0543e90..603712f8e2 100644 --- a/packages/cli/src/modules/migrate/index.ts +++ b/packages/cli/src/modules/migrate/index.ts @@ -14,9 +14,10 @@ * limitations under the License. */ import { createCliPlugin } from '@backstage/cli-plugin-api'; +import packageJson from '../../../package.json'; export default createCliPlugin({ - pluginId: 'migrate', + packageJson, init: async reg => { reg.addCommand({ path: ['versions:migrate'], diff --git a/packages/cli/src/modules/new/commands/new.test.ts b/packages/cli/src/modules/new/commands/new.test.ts index 0da42e7019..9a490e8f9a 100644 --- a/packages/cli/src/modules/new/commands/new.test.ts +++ b/packages/cli/src/modules/new/commands/new.test.ts @@ -41,7 +41,7 @@ describe.each([ } const context: CommandContext = { args, - info: { usage: 'backstage-cli new', description: 'test' }, + info: { usage: 'backstage-cli new', name: 'new' }, }; await newCommand(context); expect(createNewPackage).toHaveBeenCalledWith( diff --git a/packages/cli/src/modules/new/index.ts b/packages/cli/src/modules/new/index.ts index 2d30fb45bc..076a49f9be 100644 --- a/packages/cli/src/modules/new/index.ts +++ b/packages/cli/src/modules/new/index.ts @@ -15,9 +15,10 @@ */ import { createCliPlugin } from '@backstage/cli-plugin-api'; import { NotImplementedError } from '@backstage/errors'; +import packageJson from '../../../package.json'; export default createCliPlugin({ - pluginId: 'new', + packageJson, init: async reg => { reg.addCommand({ path: ['new'], diff --git a/packages/cli/src/modules/test/index.ts b/packages/cli/src/modules/test/index.ts index f991d23ce2..596572cb9a 100644 --- a/packages/cli/src/modules/test/index.ts +++ b/packages/cli/src/modules/test/index.ts @@ -14,9 +14,10 @@ * limitations under the License. */ import { createCliPlugin } from '@backstage/cli-plugin-api'; +import packageJson from '../../../package.json'; export default createCliPlugin({ - pluginId: 'test', + packageJson, init: async reg => { reg.addCommand({ path: ['repo', 'test'], diff --git a/packages/cli/src/modules/translations/index.ts b/packages/cli/src/modules/translations/index.ts index 9a001bab23..16c19d9345 100644 --- a/packages/cli/src/modules/translations/index.ts +++ b/packages/cli/src/modules/translations/index.ts @@ -14,9 +14,10 @@ * limitations under the License. */ import { createCliPlugin } from '@backstage/cli-plugin-api'; +import packageJson from '../../../package.json'; export default createCliPlugin({ - pluginId: 'translations', + packageJson, init: async reg => { reg.addCommand({ path: ['translations', 'export'], diff --git a/packages/cli/src/wiring/CliInitializer.test.ts b/packages/cli/src/wiring/CliInitializer.test.ts index 10de6d8e09..c5d304db91 100644 --- a/packages/cli/src/wiring/CliInitializer.test.ts +++ b/packages/cli/src/wiring/CliInitializer.test.ts @@ -29,7 +29,7 @@ describe('CliInitializer', () => { const initializer = new CliInitializer(); initializer.add( createCliPlugin({ - pluginId: 'test', + packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ path: ['test'], @@ -51,7 +51,7 @@ describe('CliInitializer', () => { const initializer = new CliInitializer(); initializer.add( createCliPlugin({ - pluginId: 'test', + packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ path: ['test'], @@ -73,7 +73,7 @@ describe('CliInitializer', () => { const initializer = new CliInitializer(); initializer.add( createCliPlugin({ - pluginId: 'test', + packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ path: ['test'], @@ -98,7 +98,7 @@ describe('CliInitializer', () => { const initializer = new CliInitializer(); initializer.add( createCliPlugin({ - pluginId: 'test', + packageJson: { name: '@backstage/test' }, init: async reg => { reg.addCommand({ path: ['visible'], @@ -125,7 +125,7 @@ describe('CliInitializer', () => { const initializer2 = new CliInitializer(); initializer2.add( createCliPlugin({ - pluginId: 'test', + packageJson: { name: '@backstage/test' }, init: async reg => { reg.addCommand({ path: ['visible'], @@ -153,7 +153,7 @@ describe('CliInitializer', () => { const initializer = new CliInitializer(); initializer.add( createCliPlugin({ - pluginId: 'test', + packageJson: { name: '@backstage/test' }, init: async reg => { reg.addCommand({ path: ['visible'], @@ -188,7 +188,7 @@ describe('CliInitializer', () => { const initializer = new CliInitializer(); initializer.add( createCliPlugin({ - pluginId: 'test', + packageJson: { name: '@backstage/test' }, init: async reg => { reg.addCommand({ path: ['group', 'alpha'], @@ -224,7 +224,7 @@ describe('CliInitializer', () => { const initializer = new CliInitializer(); initializer.add( createCliPlugin({ - pluginId: 'test', + packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ path: ['test', 'nested', 'command'], diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index bd31930854..2d1f94ce79 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -56,7 +56,9 @@ export class CliInitializer { async #register(feature: CliFeature) { if (OpaqueCliPlugin.isType(feature)) { const internal = OpaqueCliPlugin.toInternal(feature); - await internal.init(this.commandRegistry); + for (const command of await internal.commands) { + this.commandRegistry.addCommand(command); + } } else { throw new Error(`Unsupported feature type: ${(feature as any).$$type}`); } @@ -138,7 +140,7 @@ export class CliInitializer { args: [...positionalArgs, ...args.unknown], info: { usage: [programName, ...node.command.path].join(' '), - description: node.command.description, + name: node.command.path.join(' '), }, }; diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index 4fd58a5c73..b635e48ac0 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -16,7 +16,6 @@ export type { CommandContext, - CommandExecuteFn, BackstageCommand, CliFeature, CliPlugin, From 28a438a9f2b4e3c866b82373ad26ba2d65d42962 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 11 Mar 2026 13:29:31 +0100 Subject: [PATCH 105/188] Add documentation to cli-plugin-api and remove CliFeature type Add thorough TSDoc comments to createCliPlugin, BackstageCommand, CommandContext, and CliPlugin. Remove the CliFeature type alias in favor of using CliPlugin directly. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-plugin-api/report.api.md | 9 +-- .../cli-plugin-api/src/createCliPlugin.ts | 27 +++++++ packages/cli-plugin-api/src/index.ts | 7 +- packages/cli-plugin-api/src/types.ts | 71 ++++++++++++++++--- packages/cli/src/wiring/CliInitializer.ts | 12 ++-- packages/cli/src/wiring/types.ts | 1 - 6 files changed, 95 insertions(+), 32 deletions(-) diff --git a/packages/cli-plugin-api/report.api.md b/packages/cli-plugin-api/report.api.md index d72169495f..0821e01085 100644 --- a/packages/cli-plugin-api/report.api.md +++ b/packages/cli-plugin-api/report.api.md @@ -5,11 +5,8 @@ ```ts // @public export interface BackstageCommand { - // (undocumented) deprecated?: boolean; - // (undocumented) description: string; - // (undocumented) execute: | ((context: CommandContext) => Promise) | { @@ -17,9 +14,7 @@ export interface BackstageCommand { default: (context: CommandContext) => Promise; }>; }; - // (undocumented) experimental?: boolean; - // (undocumented) path: string[]; } @@ -34,12 +29,10 @@ export interface CliPlugin { // @public export interface CommandContext { - // (undocumented) args: string[]; - // (undocumented) info: { usage: string; - description: string; + name: string; }; } diff --git a/packages/cli-plugin-api/src/createCliPlugin.ts b/packages/cli-plugin-api/src/createCliPlugin.ts index 04a26665a6..17ddd8ebb9 100644 --- a/packages/cli-plugin-api/src/createCliPlugin.ts +++ b/packages/cli-plugin-api/src/createCliPlugin.ts @@ -20,11 +20,38 @@ import { BackstageCommand, CliPlugin } 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 { createCliPlugin } from '@backstage/cli-plugin-api'; + * import packageJson from '../package.json'; + * + * export default createCliPlugin({ + * packageJson, + * init: async reg => { + * reg.addCommand({ + * path: ['repo', 'test'], + * description: 'Run tests across the repository', + * execute: { loader: () => import('./commands/test') }, + * }); + * }, + * }); + * ``` + * * @public */ export function createCliPlugin(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: BackstageCommand) => void; }) => Promise; }): CliPlugin { diff --git a/packages/cli-plugin-api/src/index.ts b/packages/cli-plugin-api/src/index.ts index 988600280c..723b0edd55 100644 --- a/packages/cli-plugin-api/src/index.ts +++ b/packages/cli-plugin-api/src/index.ts @@ -15,9 +15,4 @@ */ export { createCliPlugin } from './createCliPlugin'; -export type { - BackstageCommand, - CommandContext, - CliPlugin, - CliFeature, -} from './types'; +export type { BackstageCommand, CommandContext, CliPlugin } from './types'; diff --git a/packages/cli-plugin-api/src/types.ts b/packages/cli-plugin-api/src/types.ts index 3bdf026490..cdb6d886ee 100644 --- a/packages/cli-plugin-api/src/types.ts +++ b/packages/cli-plugin-api/src/types.ts @@ -15,19 +15,33 @@ */ /** - * The context provided to a CLI command when it is executed. + * 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 CommandContext { + /** + * 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 usage string of the current command, for example: "backstage-cli repo test" + * The full usage string of the command including the program name, + * for example `"backstage-cli repo test"`. */ usage: string; /** - * The name of the command, for example: "repo test" + * The name of the command as defined by its path, + * for example `"repo test"`. */ name: string; }; @@ -36,13 +50,54 @@ export interface CommandContext { /** * 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 BackstageCommand { + /** + * 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: CommandContext) => Promise) | { @@ -53,14 +108,8 @@ export interface BackstageCommand { } /** - * A CLI feature, currently always a CLI plugin. - * - * @public - */ -export type CliFeature = CliPlugin; - -/** - * A Backstage CLI plugin. + * An opaque representation of a Backstage CLI plugin, created + * using {@link createCliPlugin}. * * @public */ diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index 2d1f94ce79..f63dc330e8 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -16,7 +16,7 @@ import { CommandGraph } from './CommandGraph'; import { OpaqueCliPlugin } from '@internal/cli'; -import type { BackstageCommand, CliFeature } from '@backstage/cli-plugin-api'; +import type { BackstageCommand, CliPlugin } from '@backstage/cli-plugin-api'; import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; import { version } from './version'; @@ -36,12 +36,12 @@ function isNodeHidden( return node.children.every(child => isNodeHidden(child as any)); } -type UninitializedFeature = CliFeature | Promise<{ default: CliFeature }>; +type UninitializedFeature = CliPlugin | Promise<{ default: CliPlugin }>; export class CliInitializer { private graph = new CommandGraph(); private commandRegistry = new CommandRegistry(this.graph); - #uninitiazedFeatures: Promise[] = []; + #uninitiazedFeatures: Promise[] = []; add(feature: UninitializedFeature) { if (isPromise(feature)) { @@ -53,7 +53,7 @@ export class CliInitializer { } } - async #register(feature: CliFeature) { + async #register(feature: CliPlugin) { if (OpaqueCliPlugin.isType(feature)) { const internal = OpaqueCliPlugin.toInternal(feature); for (const command of await internal.commands) { @@ -180,8 +180,8 @@ export class CliInitializer { /** @internal */ export function unwrapFeature( - feature: CliFeature | { default: CliFeature }, -): CliFeature { + feature: CliPlugin | { default: CliPlugin }, +): CliPlugin { if ('$$type' in feature) { return feature; } diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index b635e48ac0..98a20427b3 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -17,6 +17,5 @@ export type { CommandContext, BackstageCommand, - CliFeature, CliPlugin, } from '@backstage/cli-plugin-api'; From a90939e13820551f24929768d8eca986dea17d0f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Mar 2026 22:27:33 +0100 Subject: [PATCH 106/188] Use opaque types for command graph nodes Switch CommandGraph and CliInitializer to use OpaqueCommandTreeNode and OpaqueCommandLeafNode from @internal/cli instead of raw $$type markers. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../cli-internal/src/InternalCommandNode.ts | 54 +++++++++++ packages/cli-internal/src/index.ts | 9 ++ packages/cli/src/wiring/CliInitializer.ts | 52 ++++++----- packages/cli/src/wiring/CommandGraph.ts | 90 +++++++++++-------- 4 files changed, 146 insertions(+), 59 deletions(-) create mode 100644 packages/cli-internal/src/InternalCommandNode.ts diff --git a/packages/cli-internal/src/InternalCommandNode.ts b/packages/cli-internal/src/InternalCommandNode.ts new file mode 100644 index 0000000000..fec79ffb8e --- /dev/null +++ b/packages/cli-internal/src/InternalCommandNode.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 { BackstageCommand } from '@backstage/cli-plugin-api'; +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: BackstageCommand; + }; +}>({ + type: '@backstage/CommandLeafNode', + versions: ['v1'], +}); diff --git a/packages/cli-internal/src/index.ts b/packages/cli-internal/src/index.ts index a1dc8ad24c..578333ee96 100644 --- a/packages/cli-internal/src/index.ts +++ b/packages/cli-internal/src/index.ts @@ -15,3 +15,12 @@ */ export { OpaqueCliPlugin } from './InternalCliPlugin'; +export type { + CommandNode, + CommandTreeNode, + CommandLeafNode, +} from './InternalCommandNode'; +export { + OpaqueCommandTreeNode, + OpaqueCommandLeafNode, +} from './InternalCommandNode'; diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index f63dc330e8..ad9c483d33 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -15,8 +15,13 @@ */ import { CommandGraph } from './CommandGraph'; -import { OpaqueCliPlugin } from '@internal/cli'; -import type { BackstageCommand, CliPlugin } from '@backstage/cli-plugin-api'; +import { + OpaqueCliPlugin, + OpaqueCommandTreeNode, + OpaqueCommandLeafNode, +} from '@internal/cli'; +import type { CommandNode } from '@internal/cli'; +import type { CliPlugin } from '@backstage/cli-plugin-api'; import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; import { version } from './version'; @@ -25,15 +30,13 @@ import { exitWithError } from './errors'; import { ForwardedError } from '@backstage/errors'; import { isPromise } from 'node:util/types'; -function isNodeHidden( - node: - | { $$type: '@tree/leaf'; command: BackstageCommand } - | { $$type: '@tree/root'; children: unknown[] }, -): boolean { - if (node.$$type === '@tree/leaf') { - return !!node.command.deprecated || !!node.command.experimental; +function isNodeHidden(node: CommandNode): boolean { + if (OpaqueCommandLeafNode.isType(node)) { + const { command } = OpaqueCommandLeafNode.toInternal(node); + return !!command.deprecated || !!command.experimental; } - return node.children.every(child => isNodeHidden(child as any)); + const { children } = OpaqueCommandTreeNode.toInternal(node); + return children.every(child => isNodeHidden(child)); } type UninitializedFeature = CliPlugin | Promise<{ default: CliPlugin }>; @@ -92,25 +95,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]`, { + .command(`${internal.name} [command]`, { hidden: isNodeHidden(node), }) - .description(node.name); + .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 || !!node.command.experimental, + .command(internal.name, { + hidden: + !!internal.command.deprecated || !!internal.command.experimental, }) - .description(node.command.description) + .description(internal.command.description) .helpOption(false) .allowUnknownOption(true) .allowExcessArguments(true) @@ -129,7 +135,7 @@ 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; @@ -139,15 +145,15 @@ export class CliInitializer { const context = { args: [...positionalArgs, ...args.unknown], info: { - usage: [programName, ...node.command.path].join(' '), - name: node.command.path.join(' '), + usage: [programName, ...internal.command.path].join(' '), + name: internal.command.path.join(' '), }, }; - if (typeof node.command.execute === 'function') { - await node.command.execute(context); + if (typeof internal.command.execute === 'function') { + await internal.command.execute(context); } else { - const mod = await node.command.execute.loader(); + const mod = await internal.command.execute.loader(); // Handle CJS double-wrapping of default exports const fn = typeof mod.default === 'function' diff --git a/packages/cli/src/wiring/CommandGraph.ts b/packages/cli/src/wiring/CommandGraph.ts index a0a2de3305..d64d14b41c 100644 --- a/packages/cli/src/wiring/CommandGraph.ts +++ b/packages/cli/src/wiring/CommandGraph.ts @@ -13,27 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { + CommandNode, + OpaqueCommandTreeNode, + OpaqueCommandLeafNode, +} from '@internal/cli'; 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; -} - /** * 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 @@ -44,28 +35,44 @@ export class CommandGraph { 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(' ')}"`, ); } - 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(' ')}"`, ); } else { - current.push({ - $$type: '@tree/leaf', - name: path[path.length - 1], - command, - }); + current.push( + OpaqueCommandLeafNode.createInstance('v1', { + name: lastName, + command, + }), + ); } } @@ -76,26 +83,37 @@ export class CommandGraph { 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; From 7d055ef0c47afe4a6fed43f4a6bb633456a36a5e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 13 Mar 2026 07:39:15 +0100 Subject: [PATCH 107/188] Merge cli-plugin-api into cli-node Move createCliPlugin and related types from the standalone @backstage/cli-plugin-api package into @backstage/cli-node and remove the now-empty package. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/cli-node-add-cli-plugin.md | 5 ++ .changeset/cli-use-cli-plugin-api.md | 2 +- .changeset/create-cli-plugin-api.md | 5 -- packages/cli-internal/package.json | 2 +- .../cli-internal/src/InternalCliPlugin.ts | 2 +- .../cli-internal/src/InternalCommandNode.ts | 2 +- packages/cli-node/report.api.md | 40 +++++++++++++++ .../src/cli-plugin}/createCliPlugin.ts | 2 +- .../src => cli-node/src/cli-plugin}/index.ts | 0 .../src => cli-node/src/cli-plugin}/types.ts | 0 packages/cli-node/src/index.ts | 1 + packages/cli-plugin-api/.eslintrc.js | 1 - packages/cli-plugin-api/catalog-info.yaml | 10 ---- packages/cli-plugin-api/package.json | 39 --------------- packages/cli-plugin-api/report.api.md | 50 ------------------- packages/cli/package.json | 1 - packages/cli/src/modules/build/index.ts | 2 +- packages/cli/src/modules/config/index.ts | 2 +- .../src/modules/create-github-app/index.ts | 2 +- packages/cli/src/modules/info/index.ts | 2 +- packages/cli/src/modules/lint/index.ts | 2 +- packages/cli/src/modules/maintenance/index.ts | 2 +- packages/cli/src/modules/migrate/index.ts | 2 +- packages/cli/src/modules/new/index.ts | 2 +- packages/cli/src/modules/test/index.ts | 2 +- .../cli/src/modules/translations/index.ts | 2 +- packages/cli/src/wiring/CliInitializer.ts | 2 +- packages/cli/src/wiring/factory.ts | 2 +- packages/cli/src/wiring/types.ts | 2 +- yarn.lock | 12 +---- 30 files changed, 65 insertions(+), 135 deletions(-) create mode 100644 .changeset/cli-node-add-cli-plugin.md delete mode 100644 .changeset/create-cli-plugin-api.md rename packages/{cli-plugin-api/src => cli-node/src/cli-plugin}/createCliPlugin.ts (97%) rename packages/{cli-plugin-api/src => cli-node/src/cli-plugin}/index.ts (100%) rename packages/{cli-plugin-api/src => cli-node/src/cli-plugin}/types.ts (100%) delete mode 100644 packages/cli-plugin-api/.eslintrc.js delete mode 100644 packages/cli-plugin-api/catalog-info.yaml delete mode 100644 packages/cli-plugin-api/package.json delete mode 100644 packages/cli-plugin-api/report.api.md diff --git a/.changeset/cli-node-add-cli-plugin.md b/.changeset/cli-node-add-cli-plugin.md new file mode 100644 index 0000000000..b5a26979a9 --- /dev/null +++ b/.changeset/cli-node-add-cli-plugin.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': minor +--- + +Added `createCliPlugin` API and related types for building Backstage CLI plugins. diff --git a/.changeset/cli-use-cli-plugin-api.md b/.changeset/cli-use-cli-plugin-api.md index b55858ba62..cf7f7f12b5 100644 --- a/.changeset/cli-use-cli-plugin-api.md +++ b/.changeset/cli-use-cli-plugin-api.md @@ -2,4 +2,4 @@ '@backstage/cli': patch --- -Migrated CLI plugin modules to use `createCliPlugin` from the new `@backstage/cli-plugin-api` package. +Migrated CLI plugin modules to use `createCliPlugin` from `@backstage/cli-node`. diff --git a/.changeset/create-cli-plugin-api.md b/.changeset/create-cli-plugin-api.md deleted file mode 100644 index d740fe7897..0000000000 --- a/.changeset/create-cli-plugin-api.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli-plugin-api': minor ---- - -Added a new `@backstage/cli-plugin-api` package that provides the `createCliPlugin` API for building Backstage CLI plugins. diff --git a/packages/cli-internal/package.json b/packages/cli-internal/package.json index 80c68c74d3..068735bac3 100644 --- a/packages/cli-internal/package.json +++ b/packages/cli-internal/package.json @@ -22,7 +22,7 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/cli-plugin-api": "workspace:^" + "@backstage/cli-node": "workspace:^" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/packages/cli-internal/src/InternalCliPlugin.ts b/packages/cli-internal/src/InternalCliPlugin.ts index 426f3d23e6..e51b2e3f0e 100644 --- a/packages/cli-internal/src/InternalCliPlugin.ts +++ b/packages/cli-internal/src/InternalCliPlugin.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BackstageCommand, CliPlugin } from '@backstage/cli-plugin-api'; +import { BackstageCommand, CliPlugin } from '@backstage/cli-node'; import { OpaqueType } from '@internal/opaque'; export const OpaqueCliPlugin = OpaqueType.create<{ diff --git a/packages/cli-internal/src/InternalCommandNode.ts b/packages/cli-internal/src/InternalCommandNode.ts index fec79ffb8e..11147c3373 100644 --- a/packages/cli-internal/src/InternalCommandNode.ts +++ b/packages/cli-internal/src/InternalCommandNode.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BackstageCommand } from '@backstage/cli-plugin-api'; +import { BackstageCommand } from '@backstage/cli-node'; import { OpaqueType } from '@internal/opaque'; /** @internal */ diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 05d1545ae4..0ce66e7cb5 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -6,6 +6,21 @@ import { JsonValue } from '@backstage/types'; import { Package } from '@manypkg/get-packages'; +// @public +export interface BackstageCommand { + deprecated?: boolean; + description: string; + execute: + | ((context: CommandContext) => Promise) + | { + loader: () => Promise<{ + default: (context: CommandContext) => Promise; + }>; + }; + experimental?: boolean; + path: string[]; +} + // @public export type BackstagePackage = { dir: string; @@ -86,6 +101,21 @@ export interface BackstagePackageJson { version: string; } +// @public +export interface CliPlugin { + // (undocumented) + readonly $$type: '@backstage/CliPlugin'; +} + +// @public +export interface CommandContext { + args: string[]; + info: { + usage: string; + name: string; + }; +} + // @public export type ConcurrentTasksOptions = { concurrencyFactor?: number; @@ -93,6 +123,16 @@ export type ConcurrentTasksOptions = { worker: (item: TItem) => Promise; }; +// @public +export function createCliPlugin(options: { + packageJson: { + name: string; + }; + init: (registry: { + addCommand: (command: BackstageCommand) => void; + }) => Promise; +}): CliPlugin; + // @public export class GitUtils { static listChangedFiles(ref: string): Promise; diff --git a/packages/cli-plugin-api/src/createCliPlugin.ts b/packages/cli-node/src/cli-plugin/createCliPlugin.ts similarity index 97% rename from packages/cli-plugin-api/src/createCliPlugin.ts rename to packages/cli-node/src/cli-plugin/createCliPlugin.ts index 17ddd8ebb9..f007f74eb4 100644 --- a/packages/cli-plugin-api/src/createCliPlugin.ts +++ b/packages/cli-node/src/cli-plugin/createCliPlugin.ts @@ -26,7 +26,7 @@ import { BackstageCommand, CliPlugin } from './types'; * * @example * ``` - * import { createCliPlugin } from '@backstage/cli-plugin-api'; + * import { createCliPlugin } from '@backstage/cli-node'; * import packageJson from '../package.json'; * * export default createCliPlugin({ diff --git a/packages/cli-plugin-api/src/index.ts b/packages/cli-node/src/cli-plugin/index.ts similarity index 100% rename from packages/cli-plugin-api/src/index.ts rename to packages/cli-node/src/cli-plugin/index.ts diff --git a/packages/cli-plugin-api/src/types.ts b/packages/cli-node/src/cli-plugin/types.ts similarity index 100% rename from packages/cli-plugin-api/src/types.ts rename to packages/cli-node/src/cli-plugin/types.ts diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index 35074507c4..7c18070bbe 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -21,6 +21,7 @@ */ export * from './cache'; +export * from './cli-plugin'; export * from './concurrency'; export * from './git'; export * from './monorepo'; diff --git a/packages/cli-plugin-api/.eslintrc.js b/packages/cli-plugin-api/.eslintrc.js deleted file mode 100644 index e2a53a6ad2..0000000000 --- a/packages/cli-plugin-api/.eslintrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-plugin-api/catalog-info.yaml b/packages/cli-plugin-api/catalog-info.yaml deleted file mode 100644 index 3a0aca14b9..0000000000 --- a/packages/cli-plugin-api/catalog-info.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - name: backstage-cli-plugin-api - title: '@backstage/cli-plugin-api' - description: API for building Backstage CLI plugins -spec: - lifecycle: experimental - type: backstage-cli-plugin - owner: tooling-maintainers diff --git a/packages/cli-plugin-api/package.json b/packages/cli-plugin-api/package.json deleted file mode 100644 index 4d256f3c96..0000000000 --- a/packages/cli-plugin-api/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@backstage/cli-plugin-api", - "version": "0.1.0", - "description": "API for building Backstage CLI plugins", - "backstage": { - "role": "cli-plugin" - }, - "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-plugin-api" - }, - "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", - "test": "backstage-cli package test" - }, - "dependencies": { - "@backstage/errors": "workspace:^" - }, - "devDependencies": { - "@backstage/cli": "workspace:^" - } -} diff --git a/packages/cli-plugin-api/report.api.md b/packages/cli-plugin-api/report.api.md deleted file mode 100644 index 0821e01085..0000000000 --- a/packages/cli-plugin-api/report.api.md +++ /dev/null @@ -1,50 +0,0 @@ -## API Report File for "@backstage/cli-plugin-api" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -// @public -export interface BackstageCommand { - deprecated?: boolean; - description: string; - execute: - | ((context: CommandContext) => Promise) - | { - loader: () => Promise<{ - default: (context: CommandContext) => Promise; - }>; - }; - experimental?: boolean; - path: string[]; -} - -// @public -export type CliFeature = CliPlugin; - -// @public -export interface CliPlugin { - // (undocumented) - readonly $$type: '@backstage/CliPlugin'; -} - -// @public -export interface CommandContext { - args: string[]; - info: { - usage: string; - name: string; - }; -} - -// @public -export function createCliPlugin(options: { - packageJson: { - name: string; - }; - init: (registry: { - addCommand: (command: BackstageCommand) => void; - }) => Promise; -}): CliPlugin; - -// (No @packageDocumentation comment for this package) -``` diff --git a/packages/cli/package.json b/packages/cli/package.json index d309e0453c..8e03c60d0f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,7 +50,6 @@ "@backstage/catalog-model": "workspace:^", "@backstage/cli-common": "workspace:^", "@backstage/cli-node": "workspace:^", - "@backstage/cli-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/config-loader": "workspace:^", "@backstage/errors": "workspace:^", diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index 1c8d2217ed..e3c04e6325 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-plugin-api'; +import { createCliPlugin } from '@backstage/cli-node'; import packageJson from '../../../package.json'; export const buildPlugin = createCliPlugin({ diff --git a/packages/cli/src/modules/config/index.ts b/packages/cli/src/modules/config/index.ts index 3c59f96948..e2ddec3678 100644 --- a/packages/cli/src/modules/config/index.ts +++ b/packages/cli/src/modules/config/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-plugin-api'; +import { createCliPlugin } from '@backstage/cli-node'; import packageJson from '../../../package.json'; export const configOption = [ diff --git a/packages/cli/src/modules/create-github-app/index.ts b/packages/cli/src/modules/create-github-app/index.ts index 69fe5869d4..2171f2ca2e 100644 --- a/packages/cli/src/modules/create-github-app/index.ts +++ b/packages/cli/src/modules/create-github-app/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-plugin-api'; +import { createCliPlugin } from '@backstage/cli-node'; import packageJson from '../../../package.json'; export default createCliPlugin({ diff --git a/packages/cli/src/modules/info/index.ts b/packages/cli/src/modules/info/index.ts index df8a848f11..b1fa885f03 100644 --- a/packages/cli/src/modules/info/index.ts +++ b/packages/cli/src/modules/info/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-plugin-api'; +import { createCliPlugin } from '@backstage/cli-node'; import packageJson from '../../../package.json'; export default createCliPlugin({ diff --git a/packages/cli/src/modules/lint/index.ts b/packages/cli/src/modules/lint/index.ts index 51e5b981a4..7da72679d4 100644 --- a/packages/cli/src/modules/lint/index.ts +++ b/packages/cli/src/modules/lint/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-plugin-api'; +import { createCliPlugin } from '@backstage/cli-node'; import packageJson from '../../../package.json'; export default createCliPlugin({ diff --git a/packages/cli/src/modules/maintenance/index.ts b/packages/cli/src/modules/maintenance/index.ts index 142bd543f2..cf6cf00b2c 100644 --- a/packages/cli/src/modules/maintenance/index.ts +++ b/packages/cli/src/modules/maintenance/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-plugin-api'; +import { createCliPlugin } from '@backstage/cli-node'; import packageJson from '../../../package.json'; export default createCliPlugin({ diff --git a/packages/cli/src/modules/migrate/index.ts b/packages/cli/src/modules/migrate/index.ts index 603712f8e2..16cf45d244 100644 --- a/packages/cli/src/modules/migrate/index.ts +++ b/packages/cli/src/modules/migrate/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-plugin-api'; +import { createCliPlugin } from '@backstage/cli-node'; import packageJson from '../../../package.json'; export default createCliPlugin({ diff --git a/packages/cli/src/modules/new/index.ts b/packages/cli/src/modules/new/index.ts index 076a49f9be..9ceb50e272 100644 --- a/packages/cli/src/modules/new/index.ts +++ b/packages/cli/src/modules/new/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-plugin-api'; +import { createCliPlugin } from '@backstage/cli-node'; import { NotImplementedError } from '@backstage/errors'; import packageJson from '../../../package.json'; diff --git a/packages/cli/src/modules/test/index.ts b/packages/cli/src/modules/test/index.ts index 596572cb9a..07eabbf3f7 100644 --- a/packages/cli/src/modules/test/index.ts +++ b/packages/cli/src/modules/test/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-plugin-api'; +import { createCliPlugin } from '@backstage/cli-node'; import packageJson from '../../../package.json'; export default createCliPlugin({ diff --git a/packages/cli/src/modules/translations/index.ts b/packages/cli/src/modules/translations/index.ts index 16c19d9345..1903d87684 100644 --- a/packages/cli/src/modules/translations/index.ts +++ b/packages/cli/src/modules/translations/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-plugin-api'; +import { createCliPlugin } from '@backstage/cli-node'; import packageJson from '../../../package.json'; export default createCliPlugin({ diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index ad9c483d33..37156641ee 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -21,7 +21,7 @@ import { OpaqueCommandLeafNode, } from '@internal/cli'; import type { CommandNode } from '@internal/cli'; -import type { CliPlugin } from '@backstage/cli-plugin-api'; +import type { CliPlugin } from '@backstage/cli-node'; import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; import { version } from './version'; diff --git a/packages/cli/src/wiring/factory.ts b/packages/cli/src/wiring/factory.ts index 19c68e805f..e840e694c6 100644 --- a/packages/cli/src/wiring/factory.ts +++ b/packages/cli/src/wiring/factory.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { createCliPlugin } from '@backstage/cli-plugin-api'; +export { createCliPlugin } from '@backstage/cli-node'; diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index 98a20427b3..04eedfbd0d 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -18,4 +18,4 @@ export type { CommandContext, BackstageCommand, CliPlugin, -} from '@backstage/cli-plugin-api'; +} from '@backstage/cli-node'; diff --git a/yarn.lock b/yarn.lock index 2ef9ed9764..7465a75f4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2822,15 +2822,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-plugin-api@workspace:^, @backstage/cli-plugin-api@workspace:packages/cli-plugin-api": - version: 0.0.0-use.local - resolution: "@backstage/cli-plugin-api@workspace:packages/cli-plugin-api" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/errors": "workspace:^" - languageName: unknown - linkType: soft - "@backstage/cli@workspace:*, @backstage/cli@workspace:^, @backstage/cli@workspace:packages/cli": version: 0.0.0-use.local resolution: "@backstage/cli@workspace:packages/cli" @@ -2841,7 +2832,6 @@ __metadata: "@backstage/catalog-model": "workspace:^" "@backstage/cli-common": "workspace:^" "@backstage/cli-node": "workspace:^" - "@backstage/cli-plugin-api": "workspace:^" "@backstage/config": "workspace:^" "@backstage/config-loader": "workspace:^" "@backstage/core-app-api": "workspace:^" @@ -9685,7 +9675,7 @@ __metadata: resolution: "@internal/cli@workspace:packages/cli-internal" dependencies: "@backstage/cli": "workspace:^" - "@backstage/cli-plugin-api": "workspace:^" + "@backstage/cli-node": "workspace:^" languageName: unknown linkType: soft From 1929a95b97ea1eb635ee4d73894e7bcb1c4ef122 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 13 Mar 2026 07:46:09 +0100 Subject: [PATCH 108/188] Rename BackstageCommand to CliCommand and CommandContext to CliCommandContext Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../cli-internal/src/InternalCliPlugin.ts | 4 +- .../cli-internal/src/InternalCommandNode.ts | 4 +- packages/cli-node/report.api.md | 40 +++++++++---------- .../src/cli-plugin/createCliPlugin.ts | 6 +-- packages/cli-node/src/cli-plugin/index.ts | 2 +- packages/cli-node/src/cli-plugin/types.ts | 8 ++-- .../cli/src/modules/auth/commands/list.ts | 4 +- .../cli/src/modules/auth/commands/login.ts | 4 +- .../cli/src/modules/auth/commands/logout.ts | 4 +- .../src/modules/auth/commands/printToken.ts | 4 +- .../cli/src/modules/auth/commands/select.ts | 4 +- .../cli/src/modules/auth/commands/show.ts | 4 +- .../modules/build/commands/buildWorkspace.ts | 4 +- .../build/commands/package/build/command.ts | 4 +- .../modules/build/commands/package/clean.ts | 4 +- .../build/commands/package/postpack.ts | 4 +- .../modules/build/commands/package/prepack.ts | 4 +- .../build/commands/package/start/command.ts | 4 +- .../src/modules/build/commands/repo/build.ts | 4 +- .../src/modules/build/commands/repo/clean.ts | 4 +- .../src/modules/build/commands/repo/start.ts | 4 +- .../cli/src/modules/config/commands/docs.ts | 4 +- .../cli/src/modules/config/commands/print.ts | 4 +- .../cli/src/modules/config/commands/schema.ts | 4 +- .../src/modules/config/commands/validate.ts | 4 +- .../commands/create-github-app/index.ts | 4 +- .../cli/src/modules/info/commands/info.ts | 4 +- .../src/modules/lint/commands/package/lint.ts | 4 +- .../src/modules/lint/commands/repo/lint.ts | 4 +- .../modules/maintenance/commands/repo/fix.ts | 2 +- .../commands/repo/list-deprecations.ts | 4 +- .../migrate/commands/packageExports.ts | 4 +- .../migrate/commands/packageLintConfigs.ts | 4 +- .../modules/migrate/commands/packageRole.ts | 4 +- .../migrate/commands/packageScripts.ts | 4 +- .../migrate/commands/reactRouterDeps.ts | 4 +- .../modules/migrate/commands/versions/bump.ts | 4 +- .../migrate/commands/versions/migrate.ts | 4 +- .../cli/src/modules/new/commands/new.test.ts | 4 +- packages/cli/src/modules/new/commands/new.ts | 4 +- .../src/modules/test/commands/package/test.ts | 4 +- .../src/modules/test/commands/repo/test.ts | 4 +- .../modules/translations/commands/export.ts | 4 +- .../modules/translations/commands/import.ts | 4 +- packages/cli/src/wiring/CommandGraph.ts | 6 +-- packages/cli/src/wiring/CommandRegistry.ts | 4 +- packages/cli/src/wiring/types.ts | 4 +- 47 files changed, 114 insertions(+), 114 deletions(-) diff --git a/packages/cli-internal/src/InternalCliPlugin.ts b/packages/cli-internal/src/InternalCliPlugin.ts index e51b2e3f0e..4491e5d7c1 100644 --- a/packages/cli-internal/src/InternalCliPlugin.ts +++ b/packages/cli-internal/src/InternalCliPlugin.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BackstageCommand, CliPlugin } from '@backstage/cli-node'; +import { CliCommand, CliPlugin } from '@backstage/cli-node'; import { OpaqueType } from '@internal/opaque'; export const OpaqueCliPlugin = OpaqueType.create<{ @@ -22,7 +22,7 @@ export const OpaqueCliPlugin = OpaqueType.create<{ versions: { readonly version: 'v1'; readonly packageName: string; - readonly commands: Promise>; + readonly commands: Promise>; }; }>({ type: '@backstage/CliPlugin', diff --git a/packages/cli-internal/src/InternalCommandNode.ts b/packages/cli-internal/src/InternalCommandNode.ts index 11147c3373..3d93a5128d 100644 --- a/packages/cli-internal/src/InternalCommandNode.ts +++ b/packages/cli-internal/src/InternalCommandNode.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BackstageCommand } from '@backstage/cli-node'; +import { CliCommand } from '@backstage/cli-node'; import { OpaqueType } from '@internal/opaque'; /** @internal */ @@ -46,7 +46,7 @@ export const OpaqueCommandLeafNode = OpaqueType.create<{ versions: { readonly version: 'v1'; readonly name: string; - readonly command: BackstageCommand; + readonly command: CliCommand; }; }>({ type: '@backstage/CommandLeafNode', diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 0ce66e7cb5..d2d4db856d 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -6,21 +6,6 @@ import { JsonValue } from '@backstage/types'; import { Package } from '@manypkg/get-packages'; -// @public -export interface BackstageCommand { - deprecated?: boolean; - description: string; - execute: - | ((context: CommandContext) => Promise) - | { - loader: () => Promise<{ - default: (context: CommandContext) => Promise; - }>; - }; - experimental?: boolean; - path: string[]; -} - // @public export type BackstagePackage = { dir: string; @@ -102,13 +87,22 @@ export interface BackstagePackageJson { } // @public -export interface CliPlugin { - // (undocumented) - readonly $$type: '@backstage/CliPlugin'; +export interface CliCommand { + deprecated?: boolean; + description: string; + execute: + | ((context: CliCommandContext) => Promise) + | { + loader: () => Promise<{ + default: (context: CliCommandContext) => Promise; + }>; + }; + experimental?: boolean; + path: string[]; } // @public -export interface CommandContext { +export interface CliCommandContext { args: string[]; info: { usage: string; @@ -116,6 +110,12 @@ export interface CommandContext { }; } +// @public +export interface CliPlugin { + // (undocumented) + readonly $$type: '@backstage/CliPlugin'; +} + // @public export type ConcurrentTasksOptions = { concurrencyFactor?: number; @@ -129,7 +129,7 @@ export function createCliPlugin(options: { name: string; }; init: (registry: { - addCommand: (command: BackstageCommand) => void; + addCommand: (command: CliCommand) => void; }) => Promise; }): CliPlugin; diff --git a/packages/cli-node/src/cli-plugin/createCliPlugin.ts b/packages/cli-node/src/cli-plugin/createCliPlugin.ts index f007f74eb4..dcb79e0b3f 100644 --- a/packages/cli-node/src/cli-plugin/createCliPlugin.ts +++ b/packages/cli-node/src/cli-plugin/createCliPlugin.ts @@ -15,7 +15,7 @@ */ import { OpaqueCliPlugin } from '@internal/cli'; -import { BackstageCommand, CliPlugin } from './types'; +import { CliCommand, CliPlugin } from './types'; /** * Creates a new CLI plugin that provides commands to the Backstage CLI. @@ -52,10 +52,10 @@ export function createCliPlugin(options: { */ init: (registry: { /** Registers a new command with the CLI. */ - addCommand: (command: BackstageCommand) => void; + addCommand: (command: CliCommand) => void; }) => Promise; }): CliPlugin { - const commands: BackstageCommand[] = []; + const commands: CliCommand[] = []; const commandsPromise = options .init({ addCommand: command => commands.push(command) }) .then(() => commands); diff --git a/packages/cli-node/src/cli-plugin/index.ts b/packages/cli-node/src/cli-plugin/index.ts index 723b0edd55..df8b340c50 100644 --- a/packages/cli-node/src/cli-plugin/index.ts +++ b/packages/cli-node/src/cli-plugin/index.ts @@ -15,4 +15,4 @@ */ export { createCliPlugin } from './createCliPlugin'; -export type { BackstageCommand, CommandContext, CliPlugin } from './types'; +export type { CliCommand, CliCommandContext, CliPlugin } from './types'; diff --git a/packages/cli-node/src/cli-plugin/types.ts b/packages/cli-node/src/cli-plugin/types.ts index cdb6d886ee..21dac09eb9 100644 --- a/packages/cli-node/src/cli-plugin/types.ts +++ b/packages/cli-node/src/cli-plugin/types.ts @@ -21,7 +21,7 @@ * * @public */ -export interface CommandContext { +export interface CliCommandContext { /** * The remaining arguments passed to the command after the command path * has been resolved. This includes both positional arguments and flags. @@ -61,7 +61,7 @@ export interface CommandContext { * * @public */ -export interface BackstageCommand { +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`. @@ -99,10 +99,10 @@ export interface BackstageCommand { * ``` */ execute: - | ((context: CommandContext) => Promise) + | ((context: CliCommandContext) => Promise) | { loader: () => Promise<{ - default: (context: CommandContext) => Promise; + default: (context: CliCommandContext) => Promise; }>; }; } diff --git a/packages/cli/src/modules/auth/commands/list.ts b/packages/cli/src/modules/auth/commands/list.ts index c245072c88..c27aace3ce 100644 --- a/packages/cli/src/modules/auth/commands/list.ts +++ b/packages/cli/src/modules/auth/commands/list.ts @@ -15,10 +15,10 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; import { getAllInstances } from '../lib/storage'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info }, undefined, args); const { instances, selected } = await getAllInstances(); diff --git a/packages/cli/src/modules/auth/commands/login.ts b/packages/cli/src/modules/auth/commands/login.ts index 777fcb1807..8bdd9e0c36 100644 --- a/packages/cli/src/modules/auth/commands/login.ts +++ b/packages/cli/src/modules/auth/commands/login.ts @@ -15,7 +15,7 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; import { startCallbackServer } from '../lib/localServer'; import { spawn } from 'node:child_process'; import { challengeFromVerifier, generateVerifier } from '../lib/pkce'; @@ -37,7 +37,7 @@ import inquirer from 'inquirer'; const TOKEN_EXCHANGE_TIMEOUT_MS = 30_000; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { backendUrl, noBrowser, instance: instanceFlag }, } = cli( diff --git a/packages/cli/src/modules/auth/commands/logout.ts b/packages/cli/src/modules/auth/commands/logout.ts index 8277ea6335..ef2e0171b9 100644 --- a/packages/cli/src/modules/auth/commands/logout.ts +++ b/packages/cli/src/modules/auth/commands/logout.ts @@ -15,7 +15,7 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; import { getSecretStore } from '../lib/secretStore'; import { removeInstance, @@ -25,7 +25,7 @@ import { import { httpJson } from '../lib/http'; import { pickInstance } from '../lib/prompt'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { instance: instanceFlag }, } = cli( diff --git a/packages/cli/src/modules/auth/commands/printToken.ts b/packages/cli/src/modules/auth/commands/printToken.ts index ea848a112a..e857e39779 100644 --- a/packages/cli/src/modules/auth/commands/printToken.ts +++ b/packages/cli/src/modules/auth/commands/printToken.ts @@ -15,12 +15,12 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; import { accessTokenNeedsRefresh, refreshAccessToken } from '../lib/auth'; import { getSelectedInstance } from '../lib/storage'; import { getSecretStore } from '../lib/secretStore'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { instance: instanceFlag }, } = cli( diff --git a/packages/cli/src/modules/auth/commands/select.ts b/packages/cli/src/modules/auth/commands/select.ts index 839fee691e..713b293723 100644 --- a/packages/cli/src/modules/auth/commands/select.ts +++ b/packages/cli/src/modules/auth/commands/select.ts @@ -15,11 +15,11 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; import { setSelectedInstance } from '../lib/storage'; import { pickInstance } from '../lib/prompt'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { instance: instanceFlag }, } = cli( diff --git a/packages/cli/src/modules/auth/commands/show.ts b/packages/cli/src/modules/auth/commands/show.ts index cd55430f91..8f4ffacfe0 100644 --- a/packages/cli/src/modules/auth/commands/show.ts +++ b/packages/cli/src/modules/auth/commands/show.ts @@ -15,13 +15,13 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; 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 }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { instance: instanceFlag }, } = cli( diff --git a/packages/cli/src/modules/build/commands/buildWorkspace.ts b/packages/cli/src/modules/build/commands/buildWorkspace.ts index f9ca19962f..fd1ebe16e6 100644 --- a/packages/cli/src/modules/build/commands/buildWorkspace.ts +++ b/packages/cli/src/modules/build/commands/buildWorkspace.ts @@ -17,9 +17,9 @@ import fs from 'fs-extra'; import { cli } from 'cleye'; import { createDistWorkspace } from '../lib/packager'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +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 => { diff --git a/packages/cli/src/modules/build/commands/package/build/command.ts b/packages/cli/src/modules/build/commands/package/build/command.ts index 7a6280299b..a5ba12ea21 100644 --- a/packages/cli/src/modules/build/commands/package/build/command.ts +++ b/packages/cli/src/modules/build/commands/package/build/command.ts @@ -29,9 +29,9 @@ import { buildFrontend } from '../../../lib/buildFrontend'; import { buildBackend } from '../../../lib/buildBackend'; import { isValidUrl } from '../../../lib/urls'; import chalk from 'chalk'; -import type { CommandContext } from '../../../../../wiring/types'; +import type { CliCommandContext } from '../../../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { role, diff --git a/packages/cli/src/modules/build/commands/package/clean.ts b/packages/cli/src/modules/build/commands/package/clean.ts index 6aa987f6c1..9e7ca53d86 100644 --- a/packages/cli/src/modules/build/commands/package/clean.ts +++ b/packages/cli/src/modules/build/commands/package/clean.ts @@ -17,9 +17,9 @@ import { cli } from 'cleye'; import fs from 'fs-extra'; import { targetPaths } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +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')); diff --git a/packages/cli/src/modules/build/commands/package/postpack.ts b/packages/cli/src/modules/build/commands/package/postpack.ts index 232d03af6b..a4c64b6071 100644 --- a/packages/cli/src/modules/build/commands/package/postpack.ts +++ b/packages/cli/src/modules/build/commands/package/postpack.ts @@ -17,9 +17,9 @@ import { cli } from 'cleye'; import { targetPaths } from '@backstage/cli-common'; import { revertProductionPack } from '../../lib/packager/productionPack'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); await revertProductionPack(targetPaths.dir); }; diff --git a/packages/cli/src/modules/build/commands/package/prepack.ts b/packages/cli/src/modules/build/commands/package/prepack.ts index e6aba2ce3f..8a41f164b8 100644 --- a/packages/cli/src/modules/build/commands/package/prepack.ts +++ b/packages/cli/src/modules/build/commands/package/prepack.ts @@ -20,9 +20,9 @@ import { targetPaths } from '@backstage/cli-common'; import { productionPack } from '../../lib/packager/productionPack'; import { publishPreflightCheck } from '../../lib/publishing'; import { createTypeDistProject } from '../../lib/typeDistProject'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); publishPreflightCheck({ diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli/src/modules/build/commands/package/start/command.ts index fe871973de..56adb3889b 100644 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ b/packages/cli/src/modules/build/commands/package/start/command.ts @@ -19,9 +19,9 @@ import { startPackage } from './startPackage'; import { resolveLinkedWorkspace } from './resolveLinkedWorkspace'; import { findRoleFromCommand } from '../../../lib/role'; import { targetPaths } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../../wiring/types'; +import type { CliCommandContext } from '../../../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { config, diff --git a/packages/cli/src/modules/build/commands/repo/build.ts b/packages/cli/src/modules/build/commands/repo/build.ts index 7140310464..7587ca0cc7 100644 --- a/packages/cli/src/modules/build/commands/repo/build.ts +++ b/packages/cli/src/modules/build/commands/repo/build.ts @@ -29,9 +29,9 @@ import { import { buildFrontend } from '../../lib/buildFrontend'; import { buildBackend } from '../../lib/buildBackend'; import { createScriptOptionsParser } from '../../lib/optionsParser'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { all, since, minify }, } = cli( diff --git a/packages/cli/src/modules/build/commands/repo/clean.ts b/packages/cli/src/modules/build/commands/repo/clean.ts index 7814c1e4b7..99a6d0a77a 100644 --- a/packages/cli/src/modules/build/commands/repo/clean.ts +++ b/packages/cli/src/modules/build/commands/repo/clean.ts @@ -19,9 +19,9 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; import { run, targetPaths } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); const packages = await PackageGraph.listTargetPackages(); diff --git a/packages/cli/src/modules/build/commands/repo/start.ts b/packages/cli/src/modules/build/commands/repo/start.ts index e4c51a1e46..ab8d8f9279 100644 --- a/packages/cli/src/modules/build/commands/repo/start.ts +++ b/packages/cli/src/modules/build/commands/repo/start.ts @@ -26,7 +26,7 @@ import { cli } from 'cleye'; import { resolveLinkedWorkspace } from '../package/start/resolveLinkedWorkspace'; import { startPackage } from '../package/start/startPackage'; import { parseArgs } from 'node:util'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; const ACCEPTED_PACKAGE_ROLES: Array = [ 'frontend', @@ -35,7 +35,7 @@ const ACCEPTED_PACKAGE_ROLES: Array = [ 'backend-plugin', ]; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { plugin, config, require: requirePath, link, inspect, inspectBrk }, _: namesOrPaths, diff --git a/packages/cli/src/modules/config/commands/docs.ts b/packages/cli/src/modules/config/commands/docs.ts index 84d2f384e1..ff9955ddba 100644 --- a/packages/cli/src/modules/config/commands/docs.ts +++ b/packages/cli/src/modules/config/commands/docs.ts @@ -21,11 +21,11 @@ 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 { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; const DOCS_URL = 'https://config.backstage.io'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { package: pkg }, } = cli( diff --git a/packages/cli/src/modules/config/commands/print.ts b/packages/cli/src/modules/config/commands/print.ts index 33f7245604..fdde6fcb5d 100644 --- a/packages/cli/src/modules/config/commands/print.ts +++ b/packages/cli/src/modules/config/commands/print.ts @@ -19,9 +19,9 @@ 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 { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { config, lax, frontend, withSecrets, format, package: pkg }, } = cli( diff --git a/packages/cli/src/modules/config/commands/schema.ts b/packages/cli/src/modules/config/commands/schema.ts index 13f651534c..989fcdbc26 100644 --- a/packages/cli/src/modules/config/commands/schema.ts +++ b/packages/cli/src/modules/config/commands/schema.ts @@ -20,9 +20,9 @@ import { stringify as stringifyYaml } from 'yaml'; import { loadCliConfig } from '../lib/config'; import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { merge, format, package: pkg }, } = cli( diff --git a/packages/cli/src/modules/config/commands/validate.ts b/packages/cli/src/modules/config/commands/validate.ts index f6cf9cc662..78505f76bf 100644 --- a/packages/cli/src/modules/config/commands/validate.ts +++ b/packages/cli/src/modules/config/commands/validate.ts @@ -16,9 +16,9 @@ import { cli } from 'cleye'; import { loadCliConfig } from '../lib/config'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { config, lax, frontend, deprecated, strict, package: pkg }, } = cli( diff --git a/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts b/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts index 9dd9f4c4c4..25f3847b42 100644 --- a/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts +++ b/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts @@ -23,12 +23,12 @@ import { cli } from 'cleye'; import { GithubCreateAppServer } from './GithubCreateAppServer'; import openBrowser from 'react-dev-utils/openBrowser'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; // 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 ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { _: positionals } = cli( { help: { ...info, usage: `${info.usage} ` }, diff --git a/packages/cli/src/modules/info/commands/info.ts b/packages/cli/src/modules/info/commands/info.ts index ab26714fca..306bb54f4a 100644 --- a/packages/cli/src/modules/info/commands/info.ts +++ b/packages/cli/src/modules/info/commands/info.ts @@ -25,7 +25,7 @@ import { } from '@backstage/cli-node'; import { minimatch } from 'minimatch'; import fs from 'fs-extra'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; /** * Attempts to read package.json from node_modules for a given package name. @@ -52,7 +52,7 @@ function hasBackstageField(packageName: string, targetPath: string): boolean { return pkg?.backstage !== undefined; } -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { include, format }, } = cli( diff --git a/packages/cli/src/modules/lint/commands/package/lint.ts b/packages/cli/src/modules/lint/commands/package/lint.ts index 39c74bab73..de6716ec0a 100644 --- a/packages/cli/src/modules/lint/commands/package/lint.ts +++ b/packages/cli/src/modules/lint/commands/package/lint.ts @@ -18,9 +18,9 @@ import fs from 'fs-extra'; import { cli } from 'cleye'; import { targetPaths } from '@backstage/cli-common'; import { ESLint } from 'eslint'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { fix, format, outputFile, maxWarnings }, _: directories, diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index 5ce4be72f8..1598c44c54 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -29,7 +29,7 @@ import { import { targetPaths } from '@backstage/cli-common'; import { createScriptOptionsParser } from '../../lib/optionsParser'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; function depCount(pkg: BackstagePackageJson) { const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0; @@ -39,7 +39,7 @@ function depCount(pkg: BackstagePackageJson) { return deps + devDeps; } -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { for (const flag of [ 'outputFile', 'successCache', diff --git a/packages/cli/src/modules/maintenance/commands/repo/fix.ts b/packages/cli/src/modules/maintenance/commands/repo/fix.ts index 324b4c6a3f..d3e0759afc 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/fix.ts +++ b/packages/cli/src/modules/maintenance/commands/repo/fix.ts @@ -505,7 +505,7 @@ type PackageFixer = (pkg: FixablePackage, packages: FixablePackage[]) => void; export default async ({ args, info, -}: import('../../../../wiring/types').CommandContext) => { +}: import('../../../../wiring/types').CliCommandContext) => { const { flags: { publish, check }, } = cli( diff --git a/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts b/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts index 23f64463cb..b594920027 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts +++ b/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts @@ -20,9 +20,9 @@ import { cli } from 'cleye'; import { relative as relativePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { json }, } = cli( diff --git a/packages/cli/src/modules/migrate/commands/packageExports.ts b/packages/cli/src/modules/migrate/commands/packageExports.ts index 7fe6a49b35..68cb14a01e 100644 --- a/packages/cli/src/modules/migrate/commands/packageExports.ts +++ b/packages/cli/src/modules/migrate/commands/packageExports.ts @@ -15,9 +15,9 @@ */ import { cli } from 'cleye'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +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/src/modules/migrate/commands/packageLintConfigs.ts index 73cbe380ec..be316b0bf5 100644 --- a/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts +++ b/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts @@ -19,11 +19,11 @@ 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 { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); const packages = await PackageGraph.listTargetPackages(); diff --git a/packages/cli/src/modules/migrate/commands/packageRole.ts b/packages/cli/src/modules/migrate/commands/packageRole.ts index cc909a351c..d9f775e2d0 100644 --- a/packages/cli/src/modules/migrate/commands/packageRole.ts +++ b/packages/cli/src/modules/migrate/commands/packageRole.ts @@ -20,9 +20,9 @@ import { resolve as resolvePath } from 'node:path'; import { getPackages } from '@manypkg/get-packages'; import { PackageRoles } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); const { packages } = await getPackages(targetPaths.dir); diff --git a/packages/cli/src/modules/migrate/commands/packageScripts.ts b/packages/cli/src/modules/migrate/commands/packageScripts.ts index f9b4b56591..80810f31fc 100644 --- a/packages/cli/src/modules/migrate/commands/packageScripts.ts +++ b/packages/cli/src/modules/migrate/commands/packageScripts.ts @@ -18,13 +18,13 @@ 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 { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; const configArgPattern = /--config[=\s][^\s$]+/; const noStartRoles: PackageRole[] = ['cli', 'cli-plugin', 'common-library']; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); const packages = await PackageGraph.listTargetPackages(); diff --git a/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts b/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts index b2208e2dc4..e5a12de3cd 100644 --- a/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts +++ b/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts @@ -18,12 +18,12 @@ 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 { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; const REACT_ROUTER_DEPS = ['react-router', 'react-router-dom']; const REACT_ROUTER_RANGE = '6.0.0-beta.0 || ^6.3.0'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); const packages = await PackageGraph.listTargetPackages(); diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 8048c21505..0142e63985 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -48,7 +48,7 @@ import { import { migrateMovedPackages } from './migrate'; import { runYarnInstall } from '../../lib/utils'; import { run } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; const DEP_TYPES = [ 'dependencies', @@ -74,7 +74,7 @@ function extendsDefaultPattern(pattern: string): boolean { return minimatch('@backstage/', pattern.slice(0, -1)); } -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { pattern: patternFlag, release, skipInstall, skipMigrate }, } = cli( diff --git a/packages/cli/src/modules/migrate/commands/versions/migrate.ts b/packages/cli/src/modules/migrate/commands/versions/migrate.ts index 1286af015e..eb0596d2af 100644 --- a/packages/cli/src/modules/migrate/commands/versions/migrate.ts +++ b/packages/cli/src/modules/migrate/commands/versions/migrate.ts @@ -21,7 +21,7 @@ import { readJson, writeJson } from 'fs-extra'; import { minimatch } from 'minimatch'; import { runYarnInstall } from '../../lib/utils'; import replace from 'replace-in-file'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; declare module 'replace-in-file' { export default function (config: { @@ -39,7 +39,7 @@ declare module 'replace-in-file' { >; } -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { pattern, skipCodeChanges }, } = cli( diff --git a/packages/cli/src/modules/new/commands/new.test.ts b/packages/cli/src/modules/new/commands/new.test.ts index 9a490e8f9a..463add855d 100644 --- a/packages/cli/src/modules/new/commands/new.test.ts +++ b/packages/cli/src/modules/new/commands/new.test.ts @@ -16,7 +16,7 @@ import { createNewPackage } from '../lib/createNewPackage'; import { default as newCommand } from './new'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; jest.mock('../lib/createNewPackage'); @@ -39,7 +39,7 @@ describe.each([ if (scope) { args.push('--scope', scope); } - const context: CommandContext = { + const context: CliCommandContext = { args, info: { usage: 'backstage-cli new', name: 'new' }, }; diff --git a/packages/cli/src/modules/new/commands/new.ts b/packages/cli/src/modules/new/commands/new.ts index 3769e19269..296a3a321e 100644 --- a/packages/cli/src/modules/new/commands/new.ts +++ b/packages/cli/src/modules/new/commands/new.ts @@ -16,9 +16,9 @@ import { cli } from 'cleye'; import { createNewPackage } from '../lib/createNewPackage'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +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( diff --git a/packages/cli/src/modules/test/commands/package/test.ts b/packages/cli/src/modules/test/commands/package/test.ts index 687b66dbfd..b57ae909be 100644 --- a/packages/cli/src/modules/test/commands/package/test.ts +++ b/packages/cli/src/modules/test/commands/package/test.ts @@ -15,7 +15,7 @@ */ import { runCheck, findOwnPaths } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; function includesAnyOf(hayStack: string[], ...needles: string[]) { for (const needle of needles) { @@ -26,7 +26,7 @@ function includesAnyOf(hayStack: string[], ...needles: string[]) { return false; } -export default async ({ args }: CommandContext) => { +export default async ({ args }: CliCommandContext) => { // Only include our config if caller isn't passing their own config if (!includesAnyOf(args, '-c', '--config')) { /* eslint-disable-next-line no-restricted-syntax */ diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli/src/modules/test/commands/repo/test.ts index 1c39415192..adcaf99dfd 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.ts @@ -31,7 +31,7 @@ import { findOwnPaths, isChildPath, } from '@backstage/cli-common'; -import type { CommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '../../../../wiring/types'; type JestProject = { displayName: string; @@ -131,7 +131,7 @@ export function createFlagFinder(args: string[]) { }; } -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const testGlobal = global as TestGlobal; for (const flag of ['successCache', 'successCacheDir', 'jestHelp']) { diff --git a/packages/cli/src/modules/translations/commands/export.ts b/packages/cli/src/modules/translations/commands/export.ts index b6471f10f4..ca496ad859 100644 --- a/packages/cli/src/modules/translations/commands/export.ts +++ b/packages/cli/src/modules/translations/commands/export.ts @@ -33,9 +33,9 @@ import { formatMessagePath, validatePattern, } from '../lib/messageFilePath'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { output, pattern }, } = cli( diff --git a/packages/cli/src/modules/translations/commands/import.ts b/packages/cli/src/modules/translations/commands/import.ts index 155724dd8c..0e27321da5 100644 --- a/packages/cli/src/modules/translations/commands/import.ts +++ b/packages/cli/src/modules/translations/commands/import.ts @@ -28,7 +28,7 @@ import { createMessagePathParser, formatMessagePath, } from '../lib/messageFilePath'; -import type { CommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '../../../wiring/types'; interface ManifestRefEntry { package: string; @@ -41,7 +41,7 @@ interface Manifest { refs: Record; } -export default async ({ args, info }: CommandContext) => { +export default async ({ args, info }: CliCommandContext) => { const { flags: { input, output }, } = cli( diff --git a/packages/cli/src/wiring/CommandGraph.ts b/packages/cli/src/wiring/CommandGraph.ts index d64d14b41c..b3db5d68bb 100644 --- a/packages/cli/src/wiring/CommandGraph.ts +++ b/packages/cli/src/wiring/CommandGraph.ts @@ -18,7 +18,7 @@ import { OpaqueCommandTreeNode, OpaqueCommandLeafNode, } from '@internal/cli'; -import { BackstageCommand } from './types'; +import { CliCommand } from './types'; /** * A sparse graph of commands. @@ -30,7 +30,7 @@ export class CommandGraph { * 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) { const path = command.path; let current = this.graph; for (let i = 0; i < path.length - 1; i++) { @@ -79,7 +79,7 @@ export class CommandGraph { /** * 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]; 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/types.ts b/packages/cli/src/wiring/types.ts index 04eedfbd0d..f927645340 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -15,7 +15,7 @@ */ export type { - CommandContext, - BackstageCommand, + CliCommandContext, + CliCommand, CliPlugin, } from '@backstage/cli-node'; From 18012b5802e36527960537ffbb2d0c35217c1f67 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 13 Mar 2026 07:54:28 +0100 Subject: [PATCH 109/188] Rename CliPlugin to CliModule and cli-plugin role to cli-module Rename createCliPlugin to createCliModule, CliPlugin to CliModule, and the cli-plugin package role to cli-module to better distinguish CLI modules from other plugin types. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/add-cli-plugin-role.md | 2 +- .changeset/cli-node-add-cli-plugin.md | 2 +- .changeset/cli-support-cli-plugin-role.md | 2 +- .changeset/cli-use-cli-plugin-api.md | 2 +- ...ternalCliPlugin.ts => InternalCliModule.ts} | 8 ++++---- packages/cli-internal/src/index.ts | 2 +- packages/cli-node/report.api.md | 10 +++++----- .../createCliModule.ts} | 14 +++++++------- .../src/{cli-plugin => cli-module}/index.ts | 4 ++-- .../src/{cli-plugin => cli-module}/types.ts | 6 +++--- packages/cli-node/src/index.ts | 2 +- packages/cli-node/src/roles/PackageRoles.ts | 2 +- packages/cli-node/src/roles/types.ts | 2 +- packages/cli/config/eslint-factory.js | 2 +- packages/cli/config/jest.js | 2 +- packages/cli/src/modules/auth/index.ts | 4 ++-- packages/cli/src/modules/build/index.ts | 4 ++-- .../modules/build/lib/typeDistProject.test.ts | 2 +- packages/cli/src/modules/config/index.ts | 4 ++-- .../cli/src/modules/create-github-app/index.ts | 4 ++-- packages/cli/src/modules/info/index.ts | 4 ++-- packages/cli/src/modules/lint/index.ts | 4 ++-- .../modules/maintenance/commands/repo/fix.ts | 4 ++-- packages/cli/src/modules/maintenance/index.ts | 4 ++-- .../modules/migrate/commands/packageScripts.ts | 2 +- packages/cli/src/modules/migrate/index.ts | 4 ++-- packages/cli/src/modules/new/index.ts | 4 ++-- packages/cli/src/modules/test/index.ts | 4 ++-- packages/cli/src/modules/translations/index.ts | 4 ++-- packages/cli/src/wiring/CliInitializer.test.ts | 18 +++++++++--------- packages/cli/src/wiring/CliInitializer.ts | 18 +++++++++--------- packages/cli/src/wiring/factory.ts | 2 +- packages/cli/src/wiring/types.ts | 2 +- 33 files changed, 77 insertions(+), 77 deletions(-) rename packages/cli-internal/src/{InternalCliPlugin.ts => InternalCliModule.ts} (83%) rename packages/cli-node/src/{cli-plugin/createCliPlugin.ts => cli-module/createCliModule.ts} (86%) rename packages/cli-node/src/{cli-plugin => cli-module}/index.ts (84%) rename packages/cli-node/src/{cli-plugin => cli-module}/types.ts (96%) diff --git a/.changeset/add-cli-plugin-role.md b/.changeset/add-cli-plugin-role.md index b31c631762..1f09a08ade 100644 --- a/.changeset/add-cli-plugin-role.md +++ b/.changeset/add-cli-plugin-role.md @@ -2,4 +2,4 @@ '@backstage/cli-node': patch --- -Added a new `cli-plugin` package role for packages that provide CLI plugin extensions. +Added a new `cli-module` package role for packages that provide CLI plugin extensions. diff --git a/.changeset/cli-node-add-cli-plugin.md b/.changeset/cli-node-add-cli-plugin.md index b5a26979a9..2a6a5f20bc 100644 --- a/.changeset/cli-node-add-cli-plugin.md +++ b/.changeset/cli-node-add-cli-plugin.md @@ -2,4 +2,4 @@ '@backstage/cli-node': minor --- -Added `createCliPlugin` API and related types for building Backstage CLI plugins. +Added `createCliModule` API and related types for building Backstage CLI plugins. diff --git a/.changeset/cli-support-cli-plugin-role.md b/.changeset/cli-support-cli-plugin-role.md index d56c30db78..b336f7f551 100644 --- a/.changeset/cli-support-cli-plugin-role.md +++ b/.changeset/cli-support-cli-plugin-role.md @@ -2,4 +2,4 @@ '@backstage/cli': patch --- -Added support for the new `cli-plugin` package role in the build system, ESLint configuration, Jest configuration, and maintenance commands. +Added support for the new `cli-module` package role in the build system, ESLint configuration, Jest configuration, and maintenance commands. diff --git a/.changeset/cli-use-cli-plugin-api.md b/.changeset/cli-use-cli-plugin-api.md index cf7f7f12b5..c155483c10 100644 --- a/.changeset/cli-use-cli-plugin-api.md +++ b/.changeset/cli-use-cli-plugin-api.md @@ -2,4 +2,4 @@ '@backstage/cli': patch --- -Migrated CLI plugin modules to use `createCliPlugin` from `@backstage/cli-node`. +Migrated CLI plugin modules to use `createCliModule` from `@backstage/cli-node`. diff --git a/packages/cli-internal/src/InternalCliPlugin.ts b/packages/cli-internal/src/InternalCliModule.ts similarity index 83% rename from packages/cli-internal/src/InternalCliPlugin.ts rename to packages/cli-internal/src/InternalCliModule.ts index 4491e5d7c1..213e309371 100644 --- a/packages/cli-internal/src/InternalCliPlugin.ts +++ b/packages/cli-internal/src/InternalCliModule.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import { CliCommand, CliPlugin } from '@backstage/cli-node'; +import { CliCommand, CliModule } from '@backstage/cli-node'; import { OpaqueType } from '@internal/opaque'; -export const OpaqueCliPlugin = OpaqueType.create<{ - public: CliPlugin; +export const OpaqueCliModule = OpaqueType.create<{ + public: CliModule; versions: { readonly version: 'v1'; readonly packageName: string; readonly commands: Promise>; }; }>({ - type: '@backstage/CliPlugin', + type: '@backstage/CliModule', versions: ['v1'], }); diff --git a/packages/cli-internal/src/index.ts b/packages/cli-internal/src/index.ts index 578333ee96..68b5affe64 100644 --- a/packages/cli-internal/src/index.ts +++ b/packages/cli-internal/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { OpaqueCliPlugin } from './InternalCliPlugin'; +export { OpaqueCliModule } from './InternalCliModule'; export type { CommandNode, CommandTreeNode, diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index d2d4db856d..b979fff0b0 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -111,9 +111,9 @@ export interface CliCommandContext { } // @public -export interface CliPlugin { +export interface CliModule { // (undocumented) - readonly $$type: '@backstage/CliPlugin'; + readonly $$type: '@backstage/CliModule'; } // @public @@ -124,14 +124,14 @@ export type ConcurrentTasksOptions = { }; // @public -export function createCliPlugin(options: { +export function createCliModule(options: { packageJson: { name: string; }; init: (registry: { addCommand: (command: CliCommand) => void; }) => Promise; -}): CliPlugin; +}): CliModule; // @public export class GitUtils { @@ -227,7 +227,7 @@ export type PackageRole = | 'frontend' | 'backend' | 'cli' - | 'cli-plugin' + | 'cli-module' | 'web-library' | 'node-library' | 'common-library' diff --git a/packages/cli-node/src/cli-plugin/createCliPlugin.ts b/packages/cli-node/src/cli-module/createCliModule.ts similarity index 86% rename from packages/cli-node/src/cli-plugin/createCliPlugin.ts rename to packages/cli-node/src/cli-module/createCliModule.ts index dcb79e0b3f..89ed21876f 100644 --- a/packages/cli-node/src/cli-plugin/createCliPlugin.ts +++ b/packages/cli-node/src/cli-module/createCliModule.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { OpaqueCliPlugin } from '@internal/cli'; -import { CliCommand, CliPlugin } from './types'; +import { OpaqueCliModule } from '@internal/cli'; +import { CliCommand, CliModule } from './types'; /** * Creates a new CLI plugin that provides commands to the Backstage CLI. @@ -26,10 +26,10 @@ import { CliCommand, CliPlugin } from './types'; * * @example * ``` - * import { createCliPlugin } from '@backstage/cli-node'; + * import { createCliModule } from '@backstage/cli-node'; * import packageJson from '../package.json'; * - * export default createCliPlugin({ + * export default createCliModule({ * packageJson, * init: async reg => { * reg.addCommand({ @@ -43,7 +43,7 @@ import { CliCommand, CliPlugin } from './types'; * * @public */ -export function createCliPlugin(options: { +export function createCliModule(options: { /** The `package.json` contents of the plugin package, used to identify the plugin. */ packageJson: { name: string }; /** @@ -54,13 +54,13 @@ export function createCliPlugin(options: { /** Registers a new command with the CLI. */ addCommand: (command: CliCommand) => void; }) => Promise; -}): CliPlugin { +}): CliModule { const commands: CliCommand[] = []; const commandsPromise = options .init({ addCommand: command => commands.push(command) }) .then(() => commands); - return OpaqueCliPlugin.createInstance('v1', { + return OpaqueCliModule.createInstance('v1', { packageName: options.packageJson.name, commands: commandsPromise, }); diff --git a/packages/cli-node/src/cli-plugin/index.ts b/packages/cli-node/src/cli-module/index.ts similarity index 84% rename from packages/cli-node/src/cli-plugin/index.ts rename to packages/cli-node/src/cli-module/index.ts index df8b340c50..646073a815 100644 --- a/packages/cli-node/src/cli-plugin/index.ts +++ b/packages/cli-node/src/cli-module/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { createCliPlugin } from './createCliPlugin'; -export type { CliCommand, CliCommandContext, CliPlugin } from './types'; +export { createCliModule } from './createCliModule'; +export type { CliCommand, CliCommandContext, CliModule } from './types'; diff --git a/packages/cli-node/src/cli-plugin/types.ts b/packages/cli-node/src/cli-module/types.ts similarity index 96% rename from packages/cli-node/src/cli-plugin/types.ts rename to packages/cli-node/src/cli-module/types.ts index 21dac09eb9..3db290f89f 100644 --- a/packages/cli-node/src/cli-plugin/types.ts +++ b/packages/cli-node/src/cli-module/types.ts @@ -109,10 +109,10 @@ export interface CliCommand { /** * An opaque representation of a Backstage CLI plugin, created - * using {@link createCliPlugin}. + * using {@link createCliModule}. * * @public */ -export interface CliPlugin { - readonly $$type: '@backstage/CliPlugin'; +export interface CliModule { + readonly $$type: '@backstage/CliModule'; } diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index 7c18070bbe..8831753a82 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -21,7 +21,7 @@ */ export * from './cache'; -export * from './cli-plugin'; +export * from './cli-module'; export * from './concurrency'; export * from './git'; export * from './monorepo'; diff --git a/packages/cli-node/src/roles/PackageRoles.ts b/packages/cli-node/src/roles/PackageRoles.ts index b0ee82af84..7761b0d4cc 100644 --- a/packages/cli-node/src/roles/PackageRoles.ts +++ b/packages/cli-node/src/roles/PackageRoles.ts @@ -34,7 +34,7 @@ const packageRoleInfos: PackageRoleInfo[] = [ output: ['cjs'], }, { - role: 'cli-plugin', + role: 'cli-module', platform: 'node', output: ['types', 'cjs'], }, diff --git a/packages/cli-node/src/roles/types.ts b/packages/cli-node/src/roles/types.ts index 29e5fbb5d6..d0eaedbd02 100644 --- a/packages/cli-node/src/roles/types.ts +++ b/packages/cli-node/src/roles/types.ts @@ -23,7 +23,7 @@ export type PackageRole = | 'frontend' | 'backend' | 'cli' - | 'cli-plugin' + | 'cli-module' | 'web-library' | 'node-library' | 'common-library' diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js index db547f9b15..4e51803802 100644 --- a/packages/cli/config/eslint-factory.js +++ b/packages/cli/config/eslint-factory.js @@ -269,7 +269,7 @@ function createConfigForRole(dir, role, extraConfig = {}) { }); case 'cli': - case 'cli-plugin': + case 'cli-module': case 'node-library': case 'backend': case 'backend-plugin': diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 07f92002ae..8a88483d42 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -38,7 +38,7 @@ const FRONTEND_ROLES = [ const NODE_ROLES = [ 'backend', 'cli', - 'cli-plugin', + 'cli-module', 'node-library', 'backend-plugin', 'backend-plugin-module', diff --git a/packages/cli/src/modules/auth/index.ts b/packages/cli/src/modules/auth/index.ts index 6ce849a9b9..25876bbece 100644 --- a/packages/cli/src/modules/auth/index.ts +++ b/packages/cli/src/modules/auth/index.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliModule } from '../../wiring/factory'; import packageJson from '../../../package.json'; -export default createCliPlugin({ +export default createCliModule({ packageJson, init: async reg => { reg.addCommand({ diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index e3c04e6325..ac389c6a68 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-node'; +import { createCliModule } from '@backstage/cli-node'; import packageJson from '../../../package.json'; -export const buildPlugin = createCliPlugin({ +export const buildPlugin = createCliModule({ packageJson, init: async reg => { reg.addCommand({ diff --git a/packages/cli/src/modules/build/lib/typeDistProject.test.ts b/packages/cli/src/modules/build/lib/typeDistProject.test.ts index c773c505be..9d14bccc15 100644 --- a/packages/cli/src/modules/build/lib/typeDistProject.test.ts +++ b/packages/cli/src/modules/build/lib/typeDistProject.test.ts @@ -33,7 +33,7 @@ describe('typeDistProject', () => { frontend: false, backend: false, cli: false, - 'cli-plugin': false, + 'cli-module': false, 'common-library': false, }; diff --git a/packages/cli/src/modules/config/index.ts b/packages/cli/src/modules/config/index.ts index e2ddec3678..89a7d90111 100644 --- a/packages/cli/src/modules/config/index.ts +++ b/packages/cli/src/modules/config/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-node'; +import { createCliModule } from '@backstage/cli-node'; import packageJson from '../../../package.json'; export const configOption = [ @@ -23,7 +23,7 @@ export const configOption = [ Array(), ] as const; -export default createCliPlugin({ +export default createCliModule({ packageJson, init: async reg => { reg.addCommand({ diff --git a/packages/cli/src/modules/create-github-app/index.ts b/packages/cli/src/modules/create-github-app/index.ts index 2171f2ca2e..04786cbbdb 100644 --- a/packages/cli/src/modules/create-github-app/index.ts +++ b/packages/cli/src/modules/create-github-app/index.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-node'; +import { createCliModule } from '@backstage/cli-node'; import packageJson from '../../../package.json'; -export default createCliPlugin({ +export default createCliModule({ packageJson, init: async reg => { reg.addCommand({ diff --git a/packages/cli/src/modules/info/index.ts b/packages/cli/src/modules/info/index.ts index b1fa885f03..c4a839ff12 100644 --- a/packages/cli/src/modules/info/index.ts +++ b/packages/cli/src/modules/info/index.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-node'; +import { createCliModule } from '@backstage/cli-node'; import packageJson from '../../../package.json'; -export default createCliPlugin({ +export default createCliModule({ packageJson, init: async reg => { reg.addCommand({ diff --git a/packages/cli/src/modules/lint/index.ts b/packages/cli/src/modules/lint/index.ts index 7da72679d4..71961a147f 100644 --- a/packages/cli/src/modules/lint/index.ts +++ b/packages/cli/src/modules/lint/index.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-node'; +import { createCliModule } from '@backstage/cli-node'; import packageJson from '../../../package.json'; -export default createCliPlugin({ +export default createCliModule({ packageJson, init: async reg => { reg.addCommand({ diff --git a/packages/cli/src/modules/maintenance/commands/repo/fix.ts b/packages/cli/src/modules/maintenance/commands/repo/fix.ts index d3e0759afc..147779443d 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/fix.ts +++ b/packages/cli/src/modules/maintenance/commands/repo/fix.ts @@ -298,7 +298,7 @@ export function fixPluginId(pkg: FixablePackage) { role === 'backend' || role === 'frontend' || role === 'cli' || - role === 'cli-plugin' + role === 'cli-module' ) { return; } @@ -385,7 +385,7 @@ export function fixPluginPackages( role === 'backend' || role === 'frontend' || role === 'cli' || - role === 'cli-plugin' + role === 'cli-module' ) { return; } diff --git a/packages/cli/src/modules/maintenance/index.ts b/packages/cli/src/modules/maintenance/index.ts index cf6cf00b2c..315d222448 100644 --- a/packages/cli/src/modules/maintenance/index.ts +++ b/packages/cli/src/modules/maintenance/index.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-node'; +import { createCliModule } from '@backstage/cli-node'; import packageJson from '../../../package.json'; -export default createCliPlugin({ +export default createCliModule({ packageJson, init: async reg => { reg.addCommand({ diff --git a/packages/cli/src/modules/migrate/commands/packageScripts.ts b/packages/cli/src/modules/migrate/commands/packageScripts.ts index 80810f31fc..b9bc9d6656 100644 --- a/packages/cli/src/modules/migrate/commands/packageScripts.ts +++ b/packages/cli/src/modules/migrate/commands/packageScripts.ts @@ -22,7 +22,7 @@ import type { CliCommandContext } from '../../../wiring/types'; const configArgPattern = /--config[=\s][^\s$]+/; -const noStartRoles: PackageRole[] = ['cli', 'cli-plugin', 'common-library']; +const noStartRoles: PackageRole[] = ['cli', 'cli-module', 'common-library']; export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); diff --git a/packages/cli/src/modules/migrate/index.ts b/packages/cli/src/modules/migrate/index.ts index 16cf45d244..ca77f678a4 100644 --- a/packages/cli/src/modules/migrate/index.ts +++ b/packages/cli/src/modules/migrate/index.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-node'; +import { createCliModule } from '@backstage/cli-node'; import packageJson from '../../../package.json'; -export default createCliPlugin({ +export default createCliModule({ packageJson, init: async reg => { reg.addCommand({ diff --git a/packages/cli/src/modules/new/index.ts b/packages/cli/src/modules/new/index.ts index 9ceb50e272..2a08a71e97 100644 --- a/packages/cli/src/modules/new/index.ts +++ b/packages/cli/src/modules/new/index.ts @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-node'; +import { createCliModule } from '@backstage/cli-node'; import { NotImplementedError } from '@backstage/errors'; import packageJson from '../../../package.json'; -export default createCliPlugin({ +export default createCliModule({ packageJson, init: async reg => { reg.addCommand({ diff --git a/packages/cli/src/modules/test/index.ts b/packages/cli/src/modules/test/index.ts index 07eabbf3f7..f10e13828f 100644 --- a/packages/cli/src/modules/test/index.ts +++ b/packages/cli/src/modules/test/index.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-node'; +import { createCliModule } from '@backstage/cli-node'; import packageJson from '../../../package.json'; -export default createCliPlugin({ +export default createCliModule({ packageJson, init: async reg => { reg.addCommand({ diff --git a/packages/cli/src/modules/translations/index.ts b/packages/cli/src/modules/translations/index.ts index 1903d87684..7654c2e0b6 100644 --- a/packages/cli/src/modules/translations/index.ts +++ b/packages/cli/src/modules/translations/index.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '@backstage/cli-node'; +import { createCliModule } from '@backstage/cli-node'; import packageJson from '../../../package.json'; -export default createCliPlugin({ +export default createCliModule({ packageJson, init: async reg => { reg.addCommand({ diff --git a/packages/cli/src/wiring/CliInitializer.test.ts b/packages/cli/src/wiring/CliInitializer.test.ts index c5d304db91..b5ac9cfaaa 100644 --- a/packages/cli/src/wiring/CliInitializer.test.ts +++ b/packages/cli/src/wiring/CliInitializer.test.ts @@ -15,7 +15,7 @@ */ import { CliInitializer } from './CliInitializer'; -import { createCliPlugin } from './factory'; +import { createCliModule } from './factory'; process.exit = jest.fn() as any; @@ -28,7 +28,7 @@ describe('CliInitializer', () => { process.argv = ['node', 'cli', 'test']; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ + createCliModule({ packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ @@ -50,7 +50,7 @@ describe('CliInitializer', () => { process.argv = ['node', 'cli', 'test', '[positional]', '']; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ + createCliModule({ packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ @@ -72,7 +72,7 @@ describe('CliInitializer', () => { process.argv = ['node', 'cli', 'test', '--verbose']; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ + createCliModule({ packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ @@ -97,7 +97,7 @@ describe('CliInitializer', () => { process.argv = ['node', 'cli', 'secret']; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ + createCliModule({ packageJson: { name: '@backstage/test' }, init: async reg => { reg.addCommand({ @@ -124,7 +124,7 @@ describe('CliInitializer', () => { const writeSpy = jest.spyOn(process.stdout, 'write'); const initializer2 = new CliInitializer(); initializer2.add( - createCliPlugin({ + createCliModule({ packageJson: { name: '@backstage/test' }, init: async reg => { reg.addCommand({ @@ -152,7 +152,7 @@ describe('CliInitializer', () => { const writeSpy = jest.spyOn(process.stdout, 'write'); const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ + createCliModule({ packageJson: { name: '@backstage/test' }, init: async reg => { reg.addCommand({ @@ -187,7 +187,7 @@ describe('CliInitializer', () => { const writeSpy = jest.spyOn(process.stdout, 'write'); const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ + createCliModule({ packageJson: { name: '@backstage/test' }, init: async reg => { reg.addCommand({ @@ -223,7 +223,7 @@ describe('CliInitializer', () => { ]; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ + createCliModule({ packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index 37156641ee..a09adbbc89 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -16,12 +16,12 @@ import { CommandGraph } from './CommandGraph'; import { - OpaqueCliPlugin, + OpaqueCliModule, OpaqueCommandTreeNode, OpaqueCommandLeafNode, } from '@internal/cli'; import type { CommandNode } from '@internal/cli'; -import type { CliPlugin } from '@backstage/cli-node'; +import type { CliModule } from '@backstage/cli-node'; import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; import { version } from './version'; @@ -39,12 +39,12 @@ function isNodeHidden(node: CommandNode): boolean { return children.every(child => isNodeHidden(child)); } -type UninitializedFeature = CliPlugin | Promise<{ default: CliPlugin }>; +type UninitializedFeature = CliModule | Promise<{ default: CliModule }>; export class CliInitializer { private graph = new CommandGraph(); private commandRegistry = new CommandRegistry(this.graph); - #uninitiazedFeatures: Promise[] = []; + #uninitiazedFeatures: Promise[] = []; add(feature: UninitializedFeature) { if (isPromise(feature)) { @@ -56,9 +56,9 @@ export class CliInitializer { } } - async #register(feature: CliPlugin) { - if (OpaqueCliPlugin.isType(feature)) { - const internal = OpaqueCliPlugin.toInternal(feature); + async #register(feature: CliModule) { + if (OpaqueCliModule.isType(feature)) { + const internal = OpaqueCliModule.toInternal(feature); for (const command of await internal.commands) { this.commandRegistry.addCommand(command); } @@ -186,8 +186,8 @@ export class CliInitializer { /** @internal */ export function unwrapFeature( - feature: CliPlugin | { default: CliPlugin }, -): CliPlugin { + feature: CliModule | { default: CliModule }, +): CliModule { if ('$$type' in feature) { return feature; } diff --git a/packages/cli/src/wiring/factory.ts b/packages/cli/src/wiring/factory.ts index e840e694c6..f8f04715c3 100644 --- a/packages/cli/src/wiring/factory.ts +++ b/packages/cli/src/wiring/factory.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { createCliPlugin } from '@backstage/cli-node'; +export { createCliModule } from '@backstage/cli-node'; diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index f927645340..7a8a009a60 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -17,5 +17,5 @@ export type { CliCommandContext, CliCommand, - CliPlugin, + CliModule, } from '@backstage/cli-node'; From a151ad0814ae75904398a9555cd5600a1b311982 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 13 Mar 2026 13:43:02 +0100 Subject: [PATCH 110/188] Split CLI modules into separate packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract each CLI module from packages/cli/src/modules/ into its own package under packages/cli-module-*. This enables independent versioning and clearer dependency boundaries for each CLI capability. Module mapping: - auth → @backstage/cli-module-auth - build → @backstage/cli-module-build - config → @backstage/cli-module-config - create-github-app → @backstage/cli-module-create-github-app - info → @backstage/cli-module-info - lint → @backstage/cli-module-lint - maintenance → @backstage/cli-module-maintenance - migrate → @backstage/cli-module-migrate - new → @backstage/cli-module-new - test → @backstage/cli-module-test-jest - translations → @backstage/cli-module-translations Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-auth/.eslintrc.js | 1 + packages/cli-module-auth/catalog-info.yaml | 10 + packages/cli-module-auth/package.json | 51 ++ .../src}/commands/list.ts | 2 +- .../src}/commands/login.ts | 2 +- .../src}/commands/logout.ts | 2 +- .../src}/commands/printToken.ts | 2 +- .../src}/commands/select.ts | 2 +- .../src}/commands/show.ts | 2 +- .../auth => cli-module-auth/src}/index.ts | 4 +- .../src}/lib/auth.test.ts | 0 .../auth => cli-module-auth/src}/lib/auth.ts | 0 .../src}/lib/http.test.ts | 0 .../auth => cli-module-auth/src}/lib/http.ts | 0 .../src}/lib/localServer.test.ts | 0 .../src}/lib/localServer.ts | 0 .../src}/lib/pkce.test.ts | 0 .../auth => cli-module-auth/src}/lib/pkce.ts | 0 .../src}/lib/prompt.test.ts | 0 .../src}/lib/prompt.ts | 0 .../src}/lib/secretStore.test.ts | 0 .../src}/lib/secretStore.ts | 0 .../src}/lib/storage.test.ts | 0 .../src}/lib/storage.ts | 0 packages/cli-module-build/.eslintrc.js | 1 + packages/cli-module-build/catalog-info.yaml | 10 + packages/cli-module-build/package.json | 92 ++ .../src}/commands/buildWorkspace.ts | 2 +- .../src}/commands/package/build/command.ts | 2 +- .../src}/commands/package/build/index.ts | 0 .../src}/commands/package/clean.ts | 2 +- .../src}/commands/package/postpack.ts | 2 +- .../src}/commands/package/prepack.ts | 2 +- .../src}/commands/package/start/command.ts | 2 +- .../src}/commands/package/start/index.ts | 0 .../package/start/resolveLinkedWorkspace.ts | 0 .../commands/package/start/startBackend.ts | 0 .../commands/package/start/startFrontend.ts | 0 .../package/start/startPackage.test.ts | 0 .../commands/package/start/startPackage.ts | 0 .../src}/commands/repo/build.ts | 2 +- .../src}/commands/repo/clean.ts | 2 +- .../src}/commands/repo/start.test.ts | 0 .../src}/commands/repo/start.ts | 2 +- .../build => cli-module-build/src}/index.ts | 2 +- .../__testUtils__/createFeatureEnvironment.ts | 0 .../src}/lib/buildBackend.ts | 0 .../src}/lib/buildFrontend.ts | 0 .../src}/lib/builder/config.test.ts | 0 .../src}/lib/builder/config.ts | 0 .../src}/lib/builder/index.ts | 0 .../src}/lib/builder/packager.test.ts | 0 .../src}/lib/builder/packager.ts | 0 .../src}/lib/builder/plugins.test.ts | 0 .../src}/lib/builder/plugins.ts | 0 .../src}/lib/builder/types.ts | 0 .../ConfigInjectingHtmlWebpackPlugin.ts | 0 .../src}/lib/bundler/bundle.ts | 0 .../src}/lib/bundler/config.ts | 2 +- .../src}/lib/bundler/hasReactDomClient.ts | 0 .../src}/lib/bundler/index.ts | 0 .../src}/lib/bundler/linkWorkspaces.ts | 0 .../src}/lib/bundler/moduleFederation.test.ts | 0 .../src}/lib/bundler/moduleFederation.ts | 0 .../src}/lib/bundler/optimization.ts | 0 .../src}/lib/bundler/packageDetection.ts | 0 .../src}/lib/bundler/paths.ts | 6 +- .../src}/lib/bundler/server.ts | 0 .../src}/lib/bundler/transforms.ts | 0 .../src}/lib/bundler/types.ts | 0 .../src}/lib/config.ts | 0 .../src}/lib/entryPoints.ts | 0 .../src}/lib/ipc/IpcServer.ts | 0 .../src}/lib/ipc/ServerDataStore.ts | 0 .../src}/lib/ipc/index.ts | 0 .../src}/lib/optionsParser.ts | 0 .../src}/lib/packager/createDistWorkspace.ts | 11 +- .../src}/lib/packager/index.ts | 0 .../src}/lib/packager/productionPack.ts | 0 .../src}/lib/publishing.ts | 0 .../src}/lib/role.test.ts | 0 .../src}/lib/role.ts | 0 .../src}/lib/runner/index.ts | 0 .../src}/lib/runner/runBackend.test.ts | 0 .../src}/lib/runner/runBackend.ts | 0 .../src}/lib/typeDistProject.test.ts | 0 .../src}/lib/typeDistProject.ts | 0 .../src}/lib/urls.test.ts | 0 .../src}/lib/urls.ts | 0 .../tests/transforms/__fixtures__/.gitignore | 0 .../node_modules/dep-commonjs/a-default.js | 0 .../node_modules/dep-commonjs/a-named.js | 0 .../node_modules/dep-commonjs/b-default.mjs | 0 .../node_modules/dep-commonjs/b-named.mjs | 0 .../node_modules/dep-commonjs/c-default.cjs | 0 .../node_modules/dep-commonjs/c-named.cjs | 0 .../node_modules/dep-commonjs/main.d.ts | 0 .../node_modules/dep-commonjs/main.js | 0 .../node_modules/dep-commonjs/package.json | 0 .../node_modules/dep-default/a-default.js | 0 .../node_modules/dep-default/a-named.js | 0 .../node_modules/dep-default/b-default.mjs | 0 .../node_modules/dep-default/b-named.mjs | 0 .../node_modules/dep-default/c-default.cjs | 0 .../node_modules/dep-default/c-named.cjs | 0 .../node_modules/dep-default/main.d.ts | 0 .../node_modules/dep-default/main.js | 0 .../node_modules/dep-default/package.json | 0 .../node_modules/dep-module/a-default.js | 0 .../node_modules/dep-module/a-named.js | 0 .../node_modules/dep-module/b-default.mjs | 0 .../node_modules/dep-module/b-named.mjs | 0 .../node_modules/dep-module/c-default.cjs | 0 .../node_modules/dep-module/c-named.cjs | 0 .../node_modules/dep-module/main.d.ts | 0 .../node_modules/dep-module/main.js | 0 .../node_modules/dep-module/package.json | 0 .../__fixtures__/pkg-commonjs/a-default.ts | 0 .../__fixtures__/pkg-commonjs/a-named.ts | 0 .../__fixtures__/pkg-commonjs/b-default.mts | 0 .../__fixtures__/pkg-commonjs/b-named.mts | 0 .../__fixtures__/pkg-commonjs/c-default.cts | 0 .../__fixtures__/pkg-commonjs/c-named.cts | 0 .../__fixtures__/pkg-commonjs/main.ts | 0 .../__fixtures__/pkg-commonjs/package.json | 0 .../__fixtures__/pkg-commonjs/print.ts | 0 .../__fixtures__/pkg-default/a-default.ts | 0 .../__fixtures__/pkg-default/a-named.ts | 0 .../__fixtures__/pkg-default/b-default.mts | 0 .../__fixtures__/pkg-default/b-named.mts | 0 .../__fixtures__/pkg-default/c-default.cts | 0 .../__fixtures__/pkg-default/c-named.cts | 0 .../__fixtures__/pkg-default/main.ts | 0 .../__fixtures__/pkg-default/package.json | 0 .../__fixtures__/pkg-default/print.ts | 0 .../pkg-module/a-default-explicit.mts | 0 .../__fixtures__/pkg-module/a-default.ts | 0 .../pkg-module/a-named-explicit.mts | 0 .../__fixtures__/pkg-module/a-named.ts | 0 .../__fixtures__/pkg-module/b-default.mts | 0 .../__fixtures__/pkg-module/b-named.mts | 0 .../__fixtures__/pkg-module/c-default.cts | 0 .../__fixtures__/pkg-module/c-named.cts | 0 .../__fixtures__/pkg-module/main-explicit.mts | 0 .../__fixtures__/pkg-module/main.ts | 0 .../__fixtures__/pkg-module/package.json | 0 .../__fixtures__/pkg-module/print.ts | 0 .../src}/tests/transforms/transforms.test.ts | 0 packages/cli-module-config/.eslintrc.js | 1 + packages/cli-module-config/catalog-info.yaml | 10 + packages/cli-module-config/package.json | 50 ++ .../src}/commands/docs.ts | 2 +- .../src}/commands/print.ts | 2 +- .../src}/commands/schema.ts | 2 +- .../src}/commands/validate.ts | 2 +- .../config => cli-module-config/src}/index.ts | 2 +- .../src}/lib/config.ts | 0 .../cli-module-create-github-app/.eslintrc.js | 1 + .../catalog-info.yaml | 10 + .../cli-module-create-github-app/package.json | 50 ++ .../GithubCreateAppServer.ts | 0 .../src}/commands/create-github-app/index.ts | 2 +- .../src}/index.ts | 2 +- packages/cli-module-info/.eslintrc.js | 1 + packages/cli-module-info/catalog-info.yaml | 10 + packages/cli-module-info/package.json | 44 + .../src}/commands/info.ts | 6 +- .../info => cli-module-info/src}/index.ts | 2 +- packages/cli-module-lint/.eslintrc.js | 1 + packages/cli-module-lint/catalog-info.yaml | 10 + packages/cli-module-lint/package.json | 48 ++ .../src}/commands/package/lint.ts | 2 +- .../src}/commands/repo/lint.ts | 2 +- .../lint => cli-module-lint/src}/index.ts | 2 +- .../src}/lib/optionsParser.ts | 0 packages/cli-module-maintenance/.eslintrc.js | 1 + .../cli-module-maintenance/catalog-info.yaml | 10 + packages/cli-module-maintenance/package.json | 45 + .../src}/commands/repo/fix.ts | 2 +- .../src}/commands/repo/list-deprecations.ts | 2 +- .../src}/index.ts | 2 +- packages/cli-module-migrate/.eslintrc.js | 1 + packages/cli-module-migrate/catalog-info.yaml | 10 + packages/cli-module-migrate/package.json | 48 ++ .../src}/commands/packageExports.ts | 2 +- .../src}/commands/packageLintConfigs.ts | 2 +- .../src}/commands/packageRole.ts | 2 +- .../src}/commands/packageScripts.ts | 2 +- .../src}/commands/reactRouterDeps.ts | 2 +- .../src}/commands/versions/bump.test.ts | 0 .../src}/commands/versions/bump.ts | 2 +- .../src}/commands/versions/migrate.test.ts | 0 .../src}/commands/versions/migrate.ts | 2 +- .../src}/index.ts | 2 +- .../src}/lib/utils.ts | 0 .../src}/lib/versioning/packages.test.ts | 0 .../src}/lib/versioning/packages.ts | 0 .../src}/lib/versioning/yarn.ts | 0 packages/cli-module-new/.eslintrc.js | 1 + packages/cli-module-new/catalog-info.yaml | 10 + packages/cli-module-new/package.json | 39 + .../src}/commands/new.test.ts | 2 +- .../src}/commands/new.ts | 2 +- .../new => cli-module-new/src}/index.ts | 2 +- .../src}/lib/codeowners/codeowners.test.ts | 0 .../src}/lib/codeowners/codeowners.ts | 0 .../src}/lib/codeowners/index.ts | 0 .../src}/lib/createNewPackage.ts | 0 .../src}/lib/defaultTemplates.ts | 0 .../src}/lib/execution/PortableTemplater.ts | 0 .../lib/execution/executePortableTemplate.ts | 0 .../src}/lib/execution/index.ts | 0 .../src}/lib/execution/installNewPackage.ts | 0 .../execution/writeTemplateContents.test.ts | 0 .../lib/execution/writeTemplateContents.ts | 0 .../collectPortableTemplateInput.test.ts | 0 .../collectPortableTemplateInput.ts | 0 .../src}/lib/preparation/index.ts | 0 .../preparation/loadPortableTemplate.test.ts | 0 .../lib/preparation/loadPortableTemplate.ts | 0 .../loadPortableTemplateConfig.test.ts | 0 .../preparation/loadPortableTemplateConfig.ts | 0 .../preparation/resolvePackageParams.test.ts | 0 .../lib/preparation/resolvePackageParams.ts | 0 .../selectTemplateInteractively.test.ts | 0 .../selectTemplateInteractively.ts | 0 .../new => cli-module-new/src}/lib/tasks.ts | 0 .../new => cli-module-new/src}/lib/types.ts | 0 .../src}/lib/version.test.ts | 0 .../new => cli-module-new/src}/lib/version.ts | 46 +- packages/cli-module-test-jest/.eslintrc.js | 1 + .../cli-module-test-jest/catalog-info.yaml | 10 + packages/cli-module-test-jest/package.json | 39 + .../src}/commands/package/test.ts | 6 +- .../src}/commands/repo/test.test.ts | 0 .../src}/commands/repo/test.ts | 5 +- .../src}/index.ts | 2 +- packages/cli-module-translations/.eslintrc.js | 1 + .../cli-module-translations/catalog-info.yaml | 10 + packages/cli-module-translations/package.json | 39 + .../src}/commands/export.ts | 2 +- .../src}/commands/import.ts | 2 +- .../src}/index.ts | 2 +- .../src}/lib/discoverPackages.ts | 0 .../src}/lib/extractTranslations.test.ts | 0 .../src}/lib/extractTranslations.ts | 0 .../src}/lib/messageFilePath.test.ts | 0 .../src}/lib/messageFilePath.ts | 0 packages/cli/package.json | 11 + packages/cli/src/index.ts | 22 +- yarn.lock | 791 ++++++++++++------ 251 files changed, 1327 insertions(+), 339 deletions(-) create mode 100644 packages/cli-module-auth/.eslintrc.js create mode 100644 packages/cli-module-auth/catalog-info.yaml create mode 100644 packages/cli-module-auth/package.json rename packages/{cli/src/modules/auth => cli-module-auth/src}/commands/list.ts (94%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/commands/login.ts (99%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/commands/logout.ts (97%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/commands/printToken.ts (96%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/commands/select.ts (95%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/commands/show.ts (97%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/index.ts (94%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/auth.test.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/auth.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/http.test.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/http.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/localServer.test.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/localServer.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/pkce.test.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/pkce.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/prompt.test.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/prompt.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/secretStore.test.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/secretStore.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/storage.test.ts (100%) rename packages/{cli/src/modules/auth => cli-module-auth/src}/lib/storage.ts (100%) create mode 100644 packages/cli-module-build/.eslintrc.js create mode 100644 packages/cli-module-build/catalog-info.yaml create mode 100644 packages/cli-module-build/package.json rename packages/{cli/src/modules/build => cli-module-build/src}/commands/buildWorkspace.ts (96%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/package/build/command.ts (98%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/package/build/index.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/package/clean.ts (93%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/package/postpack.ts (93%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/package/prepack.ts (95%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/package/start/command.ts (97%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/package/start/index.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/package/start/resolveLinkedWorkspace.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/package/start/startBackend.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/package/start/startFrontend.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/package/start/startPackage.test.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/package/start/startPackage.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/repo/build.ts (98%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/repo/clean.ts (96%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/repo/start.test.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/commands/repo/start.ts (99%) rename packages/{cli/src/modules/build => cli-module-build/src}/index.ts (98%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/__testUtils__/createFeatureEnvironment.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/buildBackend.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/buildFrontend.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/builder/config.test.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/builder/config.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/builder/index.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/builder/packager.test.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/builder/packager.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/builder/plugins.test.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/builder/plugins.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/builder/types.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/bundle.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/config.ts (99%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/hasReactDomClient.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/index.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/linkWorkspaces.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/moduleFederation.test.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/moduleFederation.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/optimization.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/packageDetection.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/paths.ts (94%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/server.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/transforms.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/bundler/types.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/config.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/entryPoints.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/ipc/IpcServer.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/ipc/ServerDataStore.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/ipc/index.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/optionsParser.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/packager/createDistWorkspace.ts (98%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/packager/index.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/packager/productionPack.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/publishing.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/role.test.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/role.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/runner/index.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/runner/runBackend.test.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/runner/runBackend.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/typeDistProject.test.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/typeDistProject.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/urls.test.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/lib/urls.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/.gitignore (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-default/main.js (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-default/package.json (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-module/main.js (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/node_modules/dep-module/package.json (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-commonjs/main.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-commonjs/package.json (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-commonjs/print.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-default/a-default.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-default/a-named.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-default/b-default.mts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-default/b-named.mts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-default/c-default.cts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-default/c-named.cts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-default/main.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-default/package.json (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-default/print.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-module/a-default.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-module/a-named.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-module/b-default.mts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-module/b-named.mts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-module/c-default.cts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-module/c-named.cts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-module/main-explicit.mts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-module/main.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-module/package.json (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/__fixtures__/pkg-module/print.ts (100%) rename packages/{cli/src/modules/build => cli-module-build/src}/tests/transforms/transforms.test.ts (100%) create mode 100644 packages/cli-module-config/.eslintrc.js create mode 100644 packages/cli-module-config/catalog-info.yaml create mode 100644 packages/cli-module-config/package.json rename packages/{cli/src/modules/config => cli-module-config/src}/commands/docs.ts (97%) rename packages/{cli/src/modules/config => cli-module-config/src}/commands/print.ts (98%) rename packages/{cli/src/modules/config => cli-module-config/src}/commands/schema.ts (97%) rename packages/{cli/src/modules/config => cli-module-config/src}/commands/validate.ts (96%) rename packages/{cli/src/modules/config => cli-module-config/src}/index.ts (97%) rename packages/{cli/src/modules/config => cli-module-config/src}/lib/config.ts (100%) create mode 100644 packages/cli-module-create-github-app/.eslintrc.js create mode 100644 packages/cli-module-create-github-app/catalog-info.yaml create mode 100644 packages/cli-module-create-github-app/package.json rename packages/{cli/src/modules/create-github-app => cli-module-create-github-app/src}/commands/create-github-app/GithubCreateAppServer.ts (100%) rename packages/{cli/src/modules/create-github-app => cli-module-create-github-app/src}/commands/create-github-app/index.ts (98%) rename packages/{cli/src/modules/create-github-app => cli-module-create-github-app/src}/index.ts (95%) create mode 100644 packages/cli-module-info/.eslintrc.js create mode 100644 packages/cli-module-info/catalog-info.yaml create mode 100644 packages/cli-module-info/package.json rename packages/{cli/src/modules/info => cli-module-info/src}/commands/info.ts (97%) rename packages/{cli/src/modules/info => cli-module-info/src}/index.ts (95%) create mode 100644 packages/cli-module-lint/.eslintrc.js create mode 100644 packages/cli-module-lint/catalog-info.yaml create mode 100644 packages/cli-module-lint/package.json rename packages/{cli/src/modules/lint => cli-module-lint/src}/commands/package/lint.ts (97%) rename packages/{cli/src/modules/lint => cli-module-lint/src}/commands/repo/lint.ts (99%) rename packages/{cli/src/modules/lint => cli-module-lint/src}/index.ts (95%) rename packages/{cli/src/modules/lint => cli-module-lint/src}/lib/optionsParser.ts (100%) create mode 100644 packages/cli-module-maintenance/.eslintrc.js create mode 100644 packages/cli-module-maintenance/catalog-info.yaml create mode 100644 packages/cli-module-maintenance/package.json rename packages/{cli/src/modules/maintenance => cli-module-maintenance/src}/commands/repo/fix.ts (99%) rename packages/{cli/src/modules/maintenance => cli-module-maintenance/src}/commands/repo/list-deprecations.ts (97%) rename packages/{cli/src/modules/maintenance => cli-module-maintenance/src}/index.ts (95%) create mode 100644 packages/cli-module-migrate/.eslintrc.js create mode 100644 packages/cli-module-migrate/catalog-info.yaml create mode 100644 packages/cli-module-migrate/package.json rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/commands/packageExports.ts (93%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/commands/packageLintConfigs.ts (97%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/commands/packageRole.ts (97%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/commands/packageScripts.ts (98%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/commands/reactRouterDeps.ts (97%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/commands/versions/bump.test.ts (100%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/commands/versions/bump.ts (99%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/commands/versions/migrate.test.ts (100%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/commands/versions/migrate.ts (98%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/index.ts (98%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/lib/utils.ts (100%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/lib/versioning/packages.test.ts (100%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/lib/versioning/packages.ts (100%) rename packages/{cli/src/modules/migrate => cli-module-migrate/src}/lib/versioning/yarn.ts (100%) create mode 100644 packages/cli-module-new/.eslintrc.js create mode 100644 packages/cli-module-new/catalog-info.yaml create mode 100644 packages/cli-module-new/package.json rename packages/{cli/src/modules/new => cli-module-new/src}/commands/new.test.ts (96%) rename packages/{cli/src/modules/new => cli-module-new/src}/commands/new.ts (98%) rename packages/{cli/src/modules/new => cli-module-new/src}/index.ts (97%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/codeowners/codeowners.test.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/codeowners/codeowners.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/codeowners/index.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/createNewPackage.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/defaultTemplates.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/execution/PortableTemplater.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/execution/executePortableTemplate.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/execution/index.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/execution/installNewPackage.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/execution/writeTemplateContents.test.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/execution/writeTemplateContents.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/preparation/collectPortableTemplateInput.test.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/preparation/collectPortableTemplateInput.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/preparation/index.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/preparation/loadPortableTemplate.test.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/preparation/loadPortableTemplate.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/preparation/loadPortableTemplateConfig.test.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/preparation/loadPortableTemplateConfig.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/preparation/resolvePackageParams.test.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/preparation/resolvePackageParams.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/preparation/selectTemplateInteractively.test.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/preparation/selectTemplateInteractively.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/tasks.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/types.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/version.test.ts (100%) rename packages/{cli/src/modules/new => cli-module-new/src}/lib/version.ts (63%) create mode 100644 packages/cli-module-test-jest/.eslintrc.js create mode 100644 packages/cli-module-test-jest/catalog-info.yaml create mode 100644 packages/cli-module-test-jest/package.json rename packages/{cli/src/modules/test => cli-module-test-jest/src}/commands/package/test.ts (94%) rename packages/{cli/src/modules/test => cli-module-test-jest/src}/commands/repo/test.test.ts (100%) rename packages/{cli/src/modules/test => cli-module-test-jest/src}/commands/repo/test.ts (98%) rename packages/{cli/src/modules/test => cli-module-test-jest/src}/index.ts (96%) create mode 100644 packages/cli-module-translations/.eslintrc.js create mode 100644 packages/cli-module-translations/catalog-info.yaml create mode 100644 packages/cli-module-translations/package.json rename packages/{cli/src/modules/translations => cli-module-translations/src}/commands/export.ts (98%) rename packages/{cli/src/modules/translations => cli-module-translations/src}/commands/import.ts (99%) rename packages/{cli/src/modules/translations => cli-module-translations/src}/index.ts (96%) rename packages/{cli/src/modules/translations => cli-module-translations/src}/lib/discoverPackages.ts (100%) rename packages/{cli/src/modules/translations => cli-module-translations/src}/lib/extractTranslations.test.ts (100%) rename packages/{cli/src/modules/translations => cli-module-translations/src}/lib/extractTranslations.ts (100%) rename packages/{cli/src/modules/translations => cli-module-translations/src}/lib/messageFilePath.test.ts (100%) rename packages/{cli/src/modules/translations => cli-module-translations/src}/lib/messageFilePath.ts (100%) 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/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/package.json b/packages/cli-module-auth/package.json new file mode 100644 index 0000000000..d00b1010f6 --- /dev/null +++ b/packages/cli-module-auth/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/cli-module-auth", + "version": "0.1.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" + ], + "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", + "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", + "cross-fetch": "^4.0.0" + } +} diff --git a/packages/cli/src/modules/auth/commands/list.ts b/packages/cli-module-auth/src/commands/list.ts similarity index 94% rename from packages/cli/src/modules/auth/commands/list.ts rename to packages/cli-module-auth/src/commands/list.ts index c27aace3ce..3da2118800 100644 --- a/packages/cli/src/modules/auth/commands/list.ts +++ b/packages/cli-module-auth/src/commands/list.ts @@ -15,7 +15,7 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; import { getAllInstances } from '../lib/storage'; export default async ({ args, info }: CliCommandContext) => { diff --git a/packages/cli/src/modules/auth/commands/login.ts b/packages/cli-module-auth/src/commands/login.ts similarity index 99% rename from packages/cli/src/modules/auth/commands/login.ts rename to packages/cli-module-auth/src/commands/login.ts index 8bdd9e0c36..b16060f956 100644 --- a/packages/cli/src/modules/auth/commands/login.ts +++ b/packages/cli-module-auth/src/commands/login.ts @@ -15,7 +15,7 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; import { startCallbackServer } from '../lib/localServer'; import { spawn } from 'node:child_process'; import { challengeFromVerifier, generateVerifier } from '../lib/pkce'; diff --git a/packages/cli/src/modules/auth/commands/logout.ts b/packages/cli-module-auth/src/commands/logout.ts similarity index 97% rename from packages/cli/src/modules/auth/commands/logout.ts rename to packages/cli-module-auth/src/commands/logout.ts index ef2e0171b9..f79ed2ef35 100644 --- a/packages/cli/src/modules/auth/commands/logout.ts +++ b/packages/cli-module-auth/src/commands/logout.ts @@ -15,7 +15,7 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; import { getSecretStore } from '../lib/secretStore'; import { removeInstance, diff --git a/packages/cli/src/modules/auth/commands/printToken.ts b/packages/cli-module-auth/src/commands/printToken.ts similarity index 96% rename from packages/cli/src/modules/auth/commands/printToken.ts rename to packages/cli-module-auth/src/commands/printToken.ts index e857e39779..39a78c1832 100644 --- a/packages/cli/src/modules/auth/commands/printToken.ts +++ b/packages/cli-module-auth/src/commands/printToken.ts @@ -15,7 +15,7 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; import { accessTokenNeedsRefresh, refreshAccessToken } from '../lib/auth'; import { getSelectedInstance } from '../lib/storage'; import { getSecretStore } from '../lib/secretStore'; diff --git a/packages/cli/src/modules/auth/commands/select.ts b/packages/cli-module-auth/src/commands/select.ts similarity index 95% rename from packages/cli/src/modules/auth/commands/select.ts rename to packages/cli-module-auth/src/commands/select.ts index 713b293723..ebce23b1cb 100644 --- a/packages/cli/src/modules/auth/commands/select.ts +++ b/packages/cli-module-auth/src/commands/select.ts @@ -15,7 +15,7 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; import { setSelectedInstance } from '../lib/storage'; import { pickInstance } from '../lib/prompt'; diff --git a/packages/cli/src/modules/auth/commands/show.ts b/packages/cli-module-auth/src/commands/show.ts similarity index 97% rename from packages/cli/src/modules/auth/commands/show.ts rename to packages/cli-module-auth/src/commands/show.ts index 8f4ffacfe0..e1b7c62f72 100644 --- a/packages/cli/src/modules/auth/commands/show.ts +++ b/packages/cli-module-auth/src/commands/show.ts @@ -15,7 +15,7 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; import { httpJson } from '../lib/http'; import { getSelectedInstance } from '../lib/storage'; import { accessTokenNeedsRefresh, refreshAccessToken } from '../lib/auth'; diff --git a/packages/cli/src/modules/auth/index.ts b/packages/cli-module-auth/src/index.ts similarity index 94% rename from packages/cli/src/modules/auth/index.ts rename to packages/cli-module-auth/src/index.ts index 25876bbece..fef74ec8a5 100644 --- a/packages/cli/src/modules/auth/index.ts +++ b/packages/cli-module-auth/src/index.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { createCliModule } from '../../wiring/factory'; -import packageJson from '../../../package.json'; +import { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; export default createCliModule({ packageJson, diff --git a/packages/cli/src/modules/auth/lib/auth.test.ts b/packages/cli-module-auth/src/lib/auth.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/auth.test.ts rename to packages/cli-module-auth/src/lib/auth.test.ts diff --git a/packages/cli/src/modules/auth/lib/auth.ts b/packages/cli-module-auth/src/lib/auth.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/auth.ts rename to packages/cli-module-auth/src/lib/auth.ts diff --git a/packages/cli/src/modules/auth/lib/http.test.ts b/packages/cli-module-auth/src/lib/http.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/http.test.ts rename to packages/cli-module-auth/src/lib/http.test.ts diff --git a/packages/cli/src/modules/auth/lib/http.ts b/packages/cli-module-auth/src/lib/http.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/http.ts rename to packages/cli-module-auth/src/lib/http.ts diff --git a/packages/cli/src/modules/auth/lib/localServer.test.ts b/packages/cli-module-auth/src/lib/localServer.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/localServer.test.ts rename to packages/cli-module-auth/src/lib/localServer.test.ts diff --git a/packages/cli/src/modules/auth/lib/localServer.ts b/packages/cli-module-auth/src/lib/localServer.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/localServer.ts rename to packages/cli-module-auth/src/lib/localServer.ts diff --git a/packages/cli/src/modules/auth/lib/pkce.test.ts b/packages/cli-module-auth/src/lib/pkce.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/pkce.test.ts rename to packages/cli-module-auth/src/lib/pkce.test.ts diff --git a/packages/cli/src/modules/auth/lib/pkce.ts b/packages/cli-module-auth/src/lib/pkce.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/pkce.ts rename to packages/cli-module-auth/src/lib/pkce.ts diff --git a/packages/cli/src/modules/auth/lib/prompt.test.ts b/packages/cli-module-auth/src/lib/prompt.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/prompt.test.ts rename to packages/cli-module-auth/src/lib/prompt.test.ts diff --git a/packages/cli/src/modules/auth/lib/prompt.ts b/packages/cli-module-auth/src/lib/prompt.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/prompt.ts rename to packages/cli-module-auth/src/lib/prompt.ts diff --git a/packages/cli/src/modules/auth/lib/secretStore.test.ts b/packages/cli-module-auth/src/lib/secretStore.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/secretStore.test.ts rename to packages/cli-module-auth/src/lib/secretStore.test.ts diff --git a/packages/cli/src/modules/auth/lib/secretStore.ts b/packages/cli-module-auth/src/lib/secretStore.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/secretStore.ts rename to packages/cli-module-auth/src/lib/secretStore.ts diff --git a/packages/cli/src/modules/auth/lib/storage.test.ts b/packages/cli-module-auth/src/lib/storage.test.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/storage.test.ts rename to packages/cli-module-auth/src/lib/storage.test.ts diff --git a/packages/cli/src/modules/auth/lib/storage.ts b/packages/cli-module-auth/src/lib/storage.ts similarity index 100% rename from packages/cli/src/modules/auth/lib/storage.ts rename to packages/cli-module-auth/src/lib/storage.ts diff --git a/packages/cli-module-build/.eslintrc.js b/packages/cli-module-build/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-build/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 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/package.json b/packages/cli-module-build/package.json new file mode 100644 index 0000000000..0a4849671c --- /dev/null +++ b/packages/cli-module-build/package.json @@ -0,0 +1,92 @@ +{ + "name": "@backstage/cli-module-build", + "version": "0.1.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", + "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", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/config-loader": "workspace:^", + "@backstage/errors": "workspace:^", + "@manypkg/get-packages": "^1.1.3", + "@module-federation/enhanced": "^0.21.6", + "@pmmmwh/react-refresh-webpack-plugin": "^0.6.2", + "@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", + "bfj": "^9.0.2", + "chalk": "^4.0.0", + "chokidar": "^3.5.3", + "cleye": "^2.3.0", + "ctrlc-windows": "^2.1.0", + "esbuild-loader": "^4.4.2", + "eslint-rspack-plugin": "^4.2.1", + "eslint-webpack-plugin": "^5.0.3", + "fork-ts-checker-webpack-plugin": "^9.1.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.10.1", + "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", + "react-dev-utils": "^12.0.0-next.60", + "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", + "tar": "^7.5.6", + "ts-checker-rspack-plugin": "^1.1.5", + "webpack": "^5.105.4", + "webpack-dev-server": "^5.2.3", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/module-federation-common": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/lodash": "^4.14.151", + "@types/npm-packlist": "^3.0.0", + "@types/shell-quote": "^1.7.5", + "cross-spawn": "^7.0.6", + "rollup": "^4.59.0", + "ts-morph": "^24.0.0" + } +} diff --git a/packages/cli/src/modules/build/commands/buildWorkspace.ts b/packages/cli-module-build/src/commands/buildWorkspace.ts similarity index 96% rename from packages/cli/src/modules/build/commands/buildWorkspace.ts rename to packages/cli-module-build/src/commands/buildWorkspace.ts index fd1ebe16e6..0abd9e5307 100644 --- a/packages/cli/src/modules/build/commands/buildWorkspace.ts +++ b/packages/cli-module-build/src/commands/buildWorkspace.ts @@ -17,7 +17,7 @@ import fs from 'fs-extra'; import { cli } from 'cleye'; import { createDistWorkspace } from '../lib/packager'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { // Normalize legacy --alwaysYarnPack alias (a genuinely different name, not 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 98% 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 a5ba12ea21..07c6e3a8d7 100644 --- a/packages/cli/src/modules/build/commands/package/build/command.ts +++ b/packages/cli-module-build/src/commands/package/build/command.ts @@ -29,7 +29,7 @@ import { buildFrontend } from '../../../lib/buildFrontend'; import { buildBackend } from '../../../lib/buildBackend'; import { isValidUrl } from '../../../lib/urls'; import chalk from 'chalk'; -import type { CliCommandContext } from '../../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { const { diff --git a/packages/cli/src/modules/build/commands/package/build/index.ts b/packages/cli-module-build/src/commands/package/build/index.ts similarity index 100% rename from packages/cli/src/modules/build/commands/package/build/index.ts rename to packages/cli-module-build/src/commands/package/build/index.ts diff --git a/packages/cli/src/modules/build/commands/package/clean.ts b/packages/cli-module-build/src/commands/package/clean.ts similarity index 93% rename from packages/cli/src/modules/build/commands/package/clean.ts rename to packages/cli-module-build/src/commands/package/clean.ts index 9e7ca53d86..fffc418795 100644 --- a/packages/cli/src/modules/build/commands/package/clean.ts +++ b/packages/cli-module-build/src/commands/package/clean.ts @@ -17,7 +17,7 @@ import { cli } from 'cleye'; import fs from 'fs-extra'; import { targetPaths } from '@backstage/cli-common'; -import type { CliCommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); diff --git a/packages/cli/src/modules/build/commands/package/postpack.ts b/packages/cli-module-build/src/commands/package/postpack.ts similarity index 93% rename from packages/cli/src/modules/build/commands/package/postpack.ts rename to packages/cli-module-build/src/commands/package/postpack.ts index a4c64b6071..56856f7401 100644 --- a/packages/cli/src/modules/build/commands/package/postpack.ts +++ b/packages/cli-module-build/src/commands/package/postpack.ts @@ -17,7 +17,7 @@ import { cli } from 'cleye'; import { targetPaths } from '@backstage/cli-common'; import { revertProductionPack } from '../../lib/packager/productionPack'; -import type { CliCommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); diff --git a/packages/cli/src/modules/build/commands/package/prepack.ts b/packages/cli-module-build/src/commands/package/prepack.ts similarity index 95% rename from packages/cli/src/modules/build/commands/package/prepack.ts rename to packages/cli-module-build/src/commands/package/prepack.ts index 8a41f164b8..4488f92c76 100644 --- a/packages/cli/src/modules/build/commands/package/prepack.ts +++ b/packages/cli-module-build/src/commands/package/prepack.ts @@ -20,7 +20,7 @@ import { targetPaths } from '@backstage/cli-common'; import { productionPack } from '../../lib/packager/productionPack'; import { publishPreflightCheck } from '../../lib/publishing'; import { createTypeDistProject } from '../../lib/typeDistProject'; -import type { CliCommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli-module-build/src/commands/package/start/command.ts similarity index 97% rename from packages/cli/src/modules/build/commands/package/start/command.ts rename to packages/cli-module-build/src/commands/package/start/command.ts index 56adb3889b..8cee25732f 100644 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ b/packages/cli-module-build/src/commands/package/start/command.ts @@ -19,7 +19,7 @@ import { startPackage } from './startPackage'; import { resolveLinkedWorkspace } from './resolveLinkedWorkspace'; import { findRoleFromCommand } from '../../../lib/role'; import { targetPaths } from '@backstage/cli-common'; -import type { CliCommandContext } from '../../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { const { diff --git a/packages/cli/src/modules/build/commands/package/start/index.ts b/packages/cli-module-build/src/commands/package/start/index.ts similarity index 100% rename from packages/cli/src/modules/build/commands/package/start/index.ts rename to packages/cli-module-build/src/commands/package/start/index.ts 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 100% rename from packages/cli/src/modules/build/commands/package/start/startBackend.ts rename to packages/cli-module-build/src/commands/package/start/startBackend.ts 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 100% rename from packages/cli/src/modules/build/commands/package/start/startFrontend.ts rename to packages/cli-module-build/src/commands/package/start/startFrontend.ts 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 98% rename from packages/cli/src/modules/build/commands/repo/build.ts rename to packages/cli-module-build/src/commands/repo/build.ts index 7587ca0cc7..d0bb9cab6c 100644 --- a/packages/cli/src/modules/build/commands/repo/build.ts +++ b/packages/cli-module-build/src/commands/repo/build.ts @@ -29,7 +29,7 @@ import { import { buildFrontend } from '../../lib/buildFrontend'; import { buildBackend } from '../../lib/buildBackend'; import { createScriptOptionsParser } from '../../lib/optionsParser'; -import type { CliCommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { const { diff --git a/packages/cli/src/modules/build/commands/repo/clean.ts b/packages/cli-module-build/src/commands/repo/clean.ts similarity index 96% rename from packages/cli/src/modules/build/commands/repo/clean.ts rename to packages/cli-module-build/src/commands/repo/clean.ts index 99a6d0a77a..12b1daf911 100644 --- a/packages/cli/src/modules/build/commands/repo/clean.ts +++ b/packages/cli-module-build/src/commands/repo/clean.ts @@ -19,7 +19,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; import { run, targetPaths } from '@backstage/cli-common'; -import type { CliCommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); 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 100% rename from packages/cli/src/modules/build/commands/repo/start.test.ts rename to packages/cli-module-build/src/commands/repo/start.test.ts diff --git a/packages/cli/src/modules/build/commands/repo/start.ts b/packages/cli-module-build/src/commands/repo/start.ts similarity index 99% rename from packages/cli/src/modules/build/commands/repo/start.ts rename to packages/cli-module-build/src/commands/repo/start.ts index ab8d8f9279..8cf3dceb1d 100644 --- a/packages/cli/src/modules/build/commands/repo/start.ts +++ b/packages/cli-module-build/src/commands/repo/start.ts @@ -26,7 +26,7 @@ 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 '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; const ACCEPTED_PACKAGE_ROLES: Array = [ 'frontend', diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli-module-build/src/index.ts similarity index 98% rename from packages/cli/src/modules/build/index.ts rename to packages/cli-module-build/src/index.ts index ac389c6a68..b2fb597249 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli-module-build/src/index.ts @@ -15,7 +15,7 @@ */ import { createCliModule } from '@backstage/cli-node'; -import packageJson from '../../../package.json'; +import packageJson from '../package.json'; export const buildPlugin = createCliModule({ packageJson, diff --git a/packages/cli/src/modules/build/lib/__testUtils__/createFeatureEnvironment.ts b/packages/cli-module-build/src/lib/__testUtils__/createFeatureEnvironment.ts similarity index 100% rename from packages/cli/src/modules/build/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 100% rename from packages/cli/src/modules/build/lib/buildBackend.ts rename to packages/cli-module-build/src/lib/buildBackend.ts diff --git a/packages/cli/src/modules/build/lib/buildFrontend.ts b/packages/cli-module-build/src/lib/buildFrontend.ts similarity index 100% rename from packages/cli/src/modules/build/lib/buildFrontend.ts rename to packages/cli-module-build/src/lib/buildFrontend.ts 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 100% rename from packages/cli/src/modules/build/lib/builder/config.ts rename to packages/cli-module-build/src/lib/builder/config.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 100% rename from packages/cli/src/modules/build/lib/builder/packager.ts rename to packages/cli-module-build/src/lib/builder/packager.ts 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 100% rename from packages/cli/src/modules/build/lib/builder/plugins.ts rename to packages/cli-module-build/src/lib/builder/plugins.ts 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 99% rename from packages/cli/src/modules/build/lib/bundler/config.ts rename to packages/cli-module-build/src/lib/bundler/config.ts index c3676ecf70..9fbda57f27 100644 --- a/packages/cli/src/modules/build/lib/bundler/config.ts +++ b/packages/cli-module-build/src/lib/bundler/config.ts @@ -32,7 +32,7 @@ import pickBy from 'lodash/pickBy'; import { runOutput, targetPaths } from '@backstage/cli-common'; import { transforms } from './transforms'; -import { version } from '../../../../wiring/version'; +const { version } = require('../../../../package.json') as { version: string }; import yn from 'yn'; import { hasReactDomClient } from './hasReactDomClient'; import { createWorkspaceLinkingPlugins } from './linkWorkspaces'; diff --git a/packages/cli/src/modules/build/lib/bundler/hasReactDomClient.ts b/packages/cli-module-build/src/lib/bundler/hasReactDomClient.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/hasReactDomClient.ts rename to packages/cli-module-build/src/lib/bundler/hasReactDomClient.ts 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 100% rename from packages/cli/src/modules/build/lib/bundler/linkWorkspaces.ts rename to packages/cli-module-build/src/lib/bundler/linkWorkspaces.ts 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 100% rename from packages/cli/src/modules/build/lib/bundler/moduleFederation.ts rename to packages/cli-module-build/src/lib/bundler/moduleFederation.ts 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 100% rename from packages/cli/src/modules/build/lib/bundler/packageDetection.ts rename to packages/cli-module-build/src/lib/bundler/packageDetection.ts diff --git a/packages/cli/src/modules/build/lib/bundler/paths.ts b/packages/cli-module-build/src/lib/bundler/paths.ts similarity index 94% rename from packages/cli/src/modules/build/lib/bundler/paths.ts rename to packages/cli-module-build/src/lib/bundler/paths.ts index 5a7d3b14dc..2f2cb0d5d2 100644 --- a/packages/cli/src/modules/build/lib/bundler/paths.ts +++ b/packages/cli-module-build/src/lib/bundler/paths.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; -import { targetPaths, findOwnPaths } from '@backstage/cli-common'; +import { targetPaths } from '@backstage/cli-common'; export type BundlingPathsOptions = { // bundle entrypoint, e.g. 'src/index' @@ -50,8 +50,8 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) { targetHtml = resolvePath(targetDir, `${entry}.html`); if (!fs.pathExistsSync(targetHtml)) { /* eslint-disable-next-line no-restricted-syntax */ - targetHtml = findOwnPaths(__dirname).resolve( - 'templates/serve_index.html', + targetHtml = require.resolve( + '@backstage/cli/templates/serve_index.html', ); } } diff --git a/packages/cli/src/modules/build/lib/bundler/server.ts b/packages/cli-module-build/src/lib/bundler/server.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/server.ts rename to packages/cli-module-build/src/lib/bundler/server.ts 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 100% rename from packages/cli/src/modules/build/lib/bundler/types.ts rename to packages/cli-module-build/src/lib/bundler/types.ts diff --git a/packages/cli/src/modules/build/lib/config.ts b/packages/cli-module-build/src/lib/config.ts similarity index 100% rename from packages/cli/src/modules/build/lib/config.ts rename to packages/cli-module-build/src/lib/config.ts diff --git a/packages/cli/src/modules/build/lib/entryPoints.ts b/packages/cli-module-build/src/lib/entryPoints.ts similarity index 100% rename from packages/cli/src/modules/build/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/src/modules/build/lib/optionsParser.ts b/packages/cli-module-build/src/lib/optionsParser.ts similarity index 100% rename from packages/cli/src/modules/build/lib/optionsParser.ts rename to packages/cli-module-build/src/lib/optionsParser.ts diff --git a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts similarity index 98% rename from packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts rename to packages/cli-module-build/src/lib/packager/createDistWorkspace.ts index 9f63525186..52ceaaf355 100644 --- a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts +++ b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts @@ -27,10 +27,13 @@ import partition from 'lodash/partition'; import { run, targetPaths } from '@backstage/cli-common'; -import { - dependencies as cliDependencies, - devDependencies as cliDevDependencies, -} from '../../../../../package.json'; +const { + dependencies: cliDependencies, + devDependencies: cliDevDependencies, +} = require('../../../../package.json') as { + dependencies: Record; + devDependencies: Record; +}; import { BuildOptions, buildPackages, diff --git a/packages/cli/src/modules/build/lib/packager/index.ts b/packages/cli-module-build/src/lib/packager/index.ts similarity index 100% rename from packages/cli/src/modules/build/lib/packager/index.ts rename to packages/cli-module-build/src/lib/packager/index.ts diff --git a/packages/cli/src/modules/build/lib/packager/productionPack.ts b/packages/cli-module-build/src/lib/packager/productionPack.ts similarity index 100% rename from packages/cli/src/modules/build/lib/packager/productionPack.ts rename to packages/cli-module-build/src/lib/packager/productionPack.ts diff --git a/packages/cli/src/modules/build/lib/publishing.ts b/packages/cli-module-build/src/lib/publishing.ts similarity index 100% rename from packages/cli/src/modules/build/lib/publishing.ts rename to packages/cli-module-build/src/lib/publishing.ts diff --git a/packages/cli/src/modules/build/lib/role.test.ts b/packages/cli-module-build/src/lib/role.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/role.test.ts rename to packages/cli-module-build/src/lib/role.test.ts diff --git a/packages/cli/src/modules/build/lib/role.ts b/packages/cli-module-build/src/lib/role.ts similarity index 100% rename from packages/cli/src/modules/build/lib/role.ts rename to packages/cli-module-build/src/lib/role.ts 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 100% rename from packages/cli/src/modules/build/lib/runner/runBackend.ts rename to packages/cli-module-build/src/lib/runner/runBackend.ts diff --git a/packages/cli/src/modules/build/lib/typeDistProject.test.ts b/packages/cli-module-build/src/lib/typeDistProject.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/typeDistProject.test.ts rename to packages/cli-module-build/src/lib/typeDistProject.test.ts diff --git a/packages/cli/src/modules/build/lib/typeDistProject.ts b/packages/cli-module-build/src/lib/typeDistProject.ts similarity index 100% rename from packages/cli/src/modules/build/lib/typeDistProject.ts rename to packages/cli-module-build/src/lib/typeDistProject.ts 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/modules/build/tests/transforms/__fixtures__/.gitignore b/packages/cli-module-build/src/tests/transforms/__fixtures__/.gitignore similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/__fixtures__/.gitignore rename to packages/cli-module-build/src/tests/transforms/__fixtures__/.gitignore diff --git a/packages/cli/src/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/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/modules/build/tests/transforms/transforms.test.ts b/packages/cli-module-build/src/tests/transforms/transforms.test.ts similarity index 100% rename from packages/cli/src/modules/build/tests/transforms/transforms.test.ts rename to packages/cli-module-build/src/tests/transforms/transforms.test.ts 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/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/package.json b/packages/cli-module-config/package.json new file mode 100644 index 0000000000..841f40de35 --- /dev/null +++ b/packages/cli-module-config/package.json @@ -0,0 +1,50 @@ +{ + "name": "@backstage/cli-module-config", + "version": "0.1.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" + ], + "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", + "@types/json-schema": "^7.0.6", + "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:^" + } +} diff --git a/packages/cli/src/modules/config/commands/docs.ts b/packages/cli-module-config/src/commands/docs.ts similarity index 97% rename from packages/cli/src/modules/config/commands/docs.ts rename to packages/cli-module-config/src/commands/docs.ts index ff9955ddba..54cc1832f5 100644 --- a/packages/cli/src/modules/config/commands/docs.ts +++ b/packages/cli-module-config/src/commands/docs.ts @@ -21,7 +21,7 @@ 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 '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; const DOCS_URL = 'https://config.backstage.io'; diff --git a/packages/cli/src/modules/config/commands/print.ts b/packages/cli-module-config/src/commands/print.ts similarity index 98% rename from packages/cli/src/modules/config/commands/print.ts rename to packages/cli-module-config/src/commands/print.ts index fdde6fcb5d..08f785b1d0 100644 --- a/packages/cli/src/modules/config/commands/print.ts +++ b/packages/cli-module-config/src/commands/print.ts @@ -19,7 +19,7 @@ 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 '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { const { diff --git a/packages/cli/src/modules/config/commands/schema.ts b/packages/cli-module-config/src/commands/schema.ts similarity index 97% rename from packages/cli/src/modules/config/commands/schema.ts rename to packages/cli-module-config/src/commands/schema.ts index 989fcdbc26..4eaa8189ba 100644 --- a/packages/cli/src/modules/config/commands/schema.ts +++ b/packages/cli-module-config/src/commands/schema.ts @@ -20,7 +20,7 @@ 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 '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { const { diff --git a/packages/cli/src/modules/config/commands/validate.ts b/packages/cli-module-config/src/commands/validate.ts similarity index 96% rename from packages/cli/src/modules/config/commands/validate.ts rename to packages/cli-module-config/src/commands/validate.ts index 78505f76bf..326c6b5e42 100644 --- a/packages/cli/src/modules/config/commands/validate.ts +++ b/packages/cli-module-config/src/commands/validate.ts @@ -16,7 +16,7 @@ import { cli } from 'cleye'; import { loadCliConfig } from '../lib/config'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { const { diff --git a/packages/cli/src/modules/config/index.ts b/packages/cli-module-config/src/index.ts similarity index 97% rename from packages/cli/src/modules/config/index.ts rename to packages/cli-module-config/src/index.ts index 89a7d90111..af2a7143b7 100644 --- a/packages/cli/src/modules/config/index.ts +++ b/packages/cli-module-config/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createCliModule } from '@backstage/cli-node'; -import packageJson from '../../../package.json'; +import packageJson from '../package.json'; export const configOption = [ '--config ', diff --git a/packages/cli/src/modules/config/lib/config.ts b/packages/cli-module-config/src/lib/config.ts similarity index 100% rename from packages/cli/src/modules/config/lib/config.ts rename to packages/cli-module-config/src/lib/config.ts diff --git a/packages/cli-module-create-github-app/.eslintrc.js b/packages/cli-module-create-github-app/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-create-github-app/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-create-github-app/catalog-info.yaml b/packages/cli-module-create-github-app/catalog-info.yaml new file mode 100644 index 0000000000..66e59c5a27 --- /dev/null +++ b/packages/cli-module-create-github-app/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-create-github-app + title: '@backstage/cli-module-create-github-app' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-create-github-app/package.json b/packages/cli-module-create-github-app/package.json new file mode 100644 index 0000000000..878ac0b3f8 --- /dev/null +++ b/packages/cli-module-create-github-app/package.json @@ -0,0 +1,50 @@ +{ + "name": "@backstage/cli-module-create-github-app", + "version": "0.1.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-create-github-app" + }, + "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", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@octokit/request": "^8.0.0", + "@types/express": "^4.17.6", + "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/fs-extra": "^11.0.0" + } +} diff --git a/packages/cli/src/modules/create-github-app/commands/create-github-app/GithubCreateAppServer.ts b/packages/cli-module-create-github-app/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-create-github-app/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-create-github-app/src/commands/create-github-app/index.ts similarity index 98% rename from packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts rename to packages/cli-module-create-github-app/src/commands/create-github-app/index.ts index 25f3847b42..bb6896c9c1 100644 --- a/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts +++ b/packages/cli-module-create-github-app/src/commands/create-github-app/index.ts @@ -23,7 +23,7 @@ import { cli } from 'cleye'; import { GithubCreateAppServer } from './GithubCreateAppServer'; import openBrowser from 'react-dev-utils/openBrowser'; -import type { CliCommandContext } from '../../../../wiring/types'; +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. diff --git a/packages/cli/src/modules/create-github-app/index.ts b/packages/cli-module-create-github-app/src/index.ts similarity index 95% rename from packages/cli/src/modules/create-github-app/index.ts rename to packages/cli-module-create-github-app/src/index.ts index 04786cbbdb..c3bd7cf95f 100644 --- a/packages/cli/src/modules/create-github-app/index.ts +++ b/packages/cli-module-create-github-app/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createCliModule } from '@backstage/cli-node'; -import packageJson from '../../../package.json'; +import packageJson from '../package.json'; export default createCliModule({ packageJson, 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/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/package.json b/packages/cli-module-info/package.json new file mode 100644 index 0000000000..5e1dd894e9 --- /dev/null +++ b/packages/cli-module-info/package.json @@ -0,0 +1,44 @@ +{ + "name": "@backstage/cli-module-info", + "version": "0.1.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" + ], + "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" + } +} diff --git a/packages/cli/src/modules/info/commands/info.ts b/packages/cli-module-info/src/commands/info.ts similarity index 97% rename from packages/cli/src/modules/info/commands/info.ts rename to packages/cli-module-info/src/commands/info.ts index 306bb54f4a..dd9f892d27 100644 --- a/packages/cli/src/modules/info/commands/info.ts +++ b/packages/cli-module-info/src/commands/info.ts @@ -15,7 +15,9 @@ */ import { cli } from 'cleye'; -import { version as cliVersion } from '../../../../package.json'; +const { version: cliVersion } = require('../../../package.json') as { + version: string; +}; import os from 'node:os'; import { runOutput, targetPaths, findOwnPaths } from '@backstage/cli-common'; import { @@ -25,7 +27,7 @@ import { } from '@backstage/cli-node'; import { minimatch } from 'minimatch'; import fs from 'fs-extra'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; /** * Attempts to read package.json from node_modules for a given package name. diff --git a/packages/cli/src/modules/info/index.ts b/packages/cli-module-info/src/index.ts similarity index 95% rename from packages/cli/src/modules/info/index.ts rename to packages/cli-module-info/src/index.ts index c4a839ff12..c7ace24bbd 100644 --- a/packages/cli/src/modules/info/index.ts +++ b/packages/cli-module-info/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createCliModule } from '@backstage/cli-node'; -import packageJson from '../../../package.json'; +import packageJson from '../package.json'; export default createCliModule({ packageJson, 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/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/package.json b/packages/cli-module-lint/package.json new file mode 100644 index 0000000000..40839a0046 --- /dev/null +++ b/packages/cli-module-lint/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/cli-module-lint", + "version": "0.1.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" + ], + "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", + "globby": "^11.0.0", + "shell-quote": "^1.8.1" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/shell-quote": "^1.7.5" + } +} diff --git a/packages/cli/src/modules/lint/commands/package/lint.ts b/packages/cli-module-lint/src/commands/package/lint.ts similarity index 97% rename from packages/cli/src/modules/lint/commands/package/lint.ts rename to packages/cli-module-lint/src/commands/package/lint.ts index de6716ec0a..7fabf0c0ff 100644 --- a/packages/cli/src/modules/lint/commands/package/lint.ts +++ b/packages/cli-module-lint/src/commands/package/lint.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import { cli } from 'cleye'; import { targetPaths } from '@backstage/cli-common'; import { ESLint } from 'eslint'; -import type { CliCommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { const { diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli-module-lint/src/commands/repo/lint.ts similarity index 99% rename from packages/cli/src/modules/lint/commands/repo/lint.ts rename to packages/cli-module-lint/src/commands/repo/lint.ts index 1598c44c54..01dfad6339 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli-module-lint/src/commands/repo/lint.ts @@ -29,7 +29,7 @@ import { import { targetPaths } from '@backstage/cli-common'; import { createScriptOptionsParser } from '../../lib/optionsParser'; -import type { CliCommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; function depCount(pkg: BackstagePackageJson) { const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0; diff --git a/packages/cli/src/modules/lint/index.ts b/packages/cli-module-lint/src/index.ts similarity index 95% rename from packages/cli/src/modules/lint/index.ts rename to packages/cli-module-lint/src/index.ts index 71961a147f..d202841e79 100644 --- a/packages/cli/src/modules/lint/index.ts +++ b/packages/cli-module-lint/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createCliModule } from '@backstage/cli-node'; -import packageJson from '../../../package.json'; +import packageJson from '../package.json'; export default createCliModule({ packageJson, diff --git a/packages/cli/src/modules/lint/lib/optionsParser.ts b/packages/cli-module-lint/src/lib/optionsParser.ts similarity index 100% rename from packages/cli/src/modules/lint/lib/optionsParser.ts rename to packages/cli-module-lint/src/lib/optionsParser.ts 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/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/package.json b/packages/cli-module-maintenance/package.json new file mode 100644 index 0000000000..5d321f88f0 --- /dev/null +++ b/packages/cli-module-maintenance/package.json @@ -0,0 +1,45 @@ +{ + "name": "@backstage/cli-module-maintenance", + "version": "0.1.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" + ], + "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" + } +} diff --git a/packages/cli/src/modules/maintenance/commands/repo/fix.ts b/packages/cli-module-maintenance/src/commands/repo/fix.ts similarity index 99% rename from packages/cli/src/modules/maintenance/commands/repo/fix.ts rename to packages/cli-module-maintenance/src/commands/repo/fix.ts index 147779443d..4d6445fe86 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/fix.ts +++ b/packages/cli-module-maintenance/src/commands/repo/fix.ts @@ -505,7 +505,7 @@ type PackageFixer = (pkg: FixablePackage, packages: FixablePackage[]) => void; export default async ({ args, info, -}: import('../../../../wiring/types').CliCommandContext) => { +}: import('@backstage/cli-node').CliCommandContext) => { const { flags: { publish, check }, } = cli( 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 97% 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 b594920027..e711cb32be 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts +++ b/packages/cli-module-maintenance/src/commands/repo/list-deprecations.ts @@ -20,7 +20,7 @@ import { cli } from 'cleye'; import { relative as relativePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; -import type { CliCommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { const { diff --git a/packages/cli/src/modules/maintenance/index.ts b/packages/cli-module-maintenance/src/index.ts similarity index 95% rename from packages/cli/src/modules/maintenance/index.ts rename to packages/cli-module-maintenance/src/index.ts index 315d222448..77304eef47 100644 --- a/packages/cli/src/modules/maintenance/index.ts +++ b/packages/cli-module-maintenance/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createCliModule } from '@backstage/cli-node'; -import packageJson from '../../../package.json'; +import packageJson from '../package.json'; export default createCliModule({ packageJson, 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/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/package.json b/packages/cli-module-migrate/package.json new file mode 100644 index 0000000000..660b85dff2 --- /dev/null +++ b/packages/cli-module-migrate/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/cli-module-migrate", + "version": "0.1.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" + ], + "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:^", + "@manypkg/get-packages": "^1.1.3", + "cleye": "^2.3.0", + "fs-extra": "^11.2.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@types/fs-extra": "^11.0.0", + "msw": "^1.0.0" + } +} diff --git a/packages/cli/src/modules/migrate/commands/packageExports.ts b/packages/cli-module-migrate/src/commands/packageExports.ts similarity index 93% rename from packages/cli/src/modules/migrate/commands/packageExports.ts rename to packages/cli-module-migrate/src/commands/packageExports.ts index 68cb14a01e..8a4ad45f42 100644 --- a/packages/cli/src/modules/migrate/commands/packageExports.ts +++ b/packages/cli-module-migrate/src/commands/packageExports.ts @@ -15,7 +15,7 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); diff --git a/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts b/packages/cli-module-migrate/src/commands/packageLintConfigs.ts similarity index 97% rename from packages/cli/src/modules/migrate/commands/packageLintConfigs.ts rename to packages/cli-module-migrate/src/commands/packageLintConfigs.ts index be316b0bf5..660ed35bb6 100644 --- a/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts +++ b/packages/cli-module-migrate/src/commands/packageLintConfigs.ts @@ -19,7 +19,7 @@ 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 '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`; diff --git a/packages/cli/src/modules/migrate/commands/packageRole.ts b/packages/cli-module-migrate/src/commands/packageRole.ts similarity index 97% rename from packages/cli/src/modules/migrate/commands/packageRole.ts rename to packages/cli-module-migrate/src/commands/packageRole.ts index d9f775e2d0..8758d48dd5 100644 --- a/packages/cli/src/modules/migrate/commands/packageRole.ts +++ b/packages/cli-module-migrate/src/commands/packageRole.ts @@ -20,7 +20,7 @@ import { resolve as resolvePath } from 'node:path'; import { getPackages } from '@manypkg/get-packages'; import { PackageRoles } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { cli({ help: info, booleanFlagNegation: true }, undefined, args); diff --git a/packages/cli/src/modules/migrate/commands/packageScripts.ts b/packages/cli-module-migrate/src/commands/packageScripts.ts similarity index 98% rename from packages/cli/src/modules/migrate/commands/packageScripts.ts rename to packages/cli-module-migrate/src/commands/packageScripts.ts index b9bc9d6656..94a05f67f9 100644 --- a/packages/cli/src/modules/migrate/commands/packageScripts.ts +++ b/packages/cli-module-migrate/src/commands/packageScripts.ts @@ -18,7 +18,7 @@ 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 '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; const configArgPattern = /--config[=\s][^\s$]+/; diff --git a/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts b/packages/cli-module-migrate/src/commands/reactRouterDeps.ts similarity index 97% rename from packages/cli/src/modules/migrate/commands/reactRouterDeps.ts rename to packages/cli-module-migrate/src/commands/reactRouterDeps.ts index e5a12de3cd..802d958e61 100644 --- a/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts +++ b/packages/cli-module-migrate/src/commands/reactRouterDeps.ts @@ -18,7 +18,7 @@ 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 '../../../wiring/types'; +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'; 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 100% rename from packages/cli/src/modules/migrate/commands/versions/bump.test.ts rename to packages/cli-module-migrate/src/commands/versions/bump.test.ts diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli-module-migrate/src/commands/versions/bump.ts similarity index 99% rename from packages/cli/src/modules/migrate/commands/versions/bump.ts rename to packages/cli-module-migrate/src/commands/versions/bump.ts index 0142e63985..518126a3de 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli-module-migrate/src/commands/versions/bump.ts @@ -48,7 +48,7 @@ import { import { migrateMovedPackages } from './migrate'; import { runYarnInstall } from '../../lib/utils'; import { run } from '@backstage/cli-common'; -import type { CliCommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; const DEP_TYPES = [ 'dependencies', 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 100% rename from packages/cli/src/modules/migrate/commands/versions/migrate.test.ts rename to packages/cli-module-migrate/src/commands/versions/migrate.test.ts diff --git a/packages/cli/src/modules/migrate/commands/versions/migrate.ts b/packages/cli-module-migrate/src/commands/versions/migrate.ts similarity index 98% rename from packages/cli/src/modules/migrate/commands/versions/migrate.ts rename to packages/cli-module-migrate/src/commands/versions/migrate.ts index eb0596d2af..a8ecb6cad4 100644 --- a/packages/cli/src/modules/migrate/commands/versions/migrate.ts +++ b/packages/cli-module-migrate/src/commands/versions/migrate.ts @@ -21,7 +21,7 @@ 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 '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; declare module 'replace-in-file' { export default function (config: { diff --git a/packages/cli/src/modules/migrate/index.ts b/packages/cli-module-migrate/src/index.ts similarity index 98% rename from packages/cli/src/modules/migrate/index.ts rename to packages/cli-module-migrate/src/index.ts index ca77f678a4..e5d89383b8 100644 --- a/packages/cli/src/modules/migrate/index.ts +++ b/packages/cli-module-migrate/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createCliModule } from '@backstage/cli-node'; -import packageJson from '../../../package.json'; +import packageJson from '../package.json'; export default createCliModule({ packageJson, 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/modules/migrate/lib/versioning/packages.test.ts b/packages/cli-module-migrate/src/lib/versioning/packages.test.ts similarity index 100% rename from packages/cli/src/modules/migrate/lib/versioning/packages.test.ts rename to packages/cli-module-migrate/src/lib/versioning/packages.test.ts diff --git a/packages/cli/src/modules/migrate/lib/versioning/packages.ts b/packages/cli-module-migrate/src/lib/versioning/packages.ts similarity index 100% rename from packages/cli/src/modules/migrate/lib/versioning/packages.ts rename to packages/cli-module-migrate/src/lib/versioning/packages.ts diff --git a/packages/cli/src/modules/migrate/lib/versioning/yarn.ts b/packages/cli-module-migrate/src/lib/versioning/yarn.ts similarity index 100% rename from packages/cli/src/modules/migrate/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/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/package.json b/packages/cli-module-new/package.json new file mode 100644 index 0000000000..5a04becd22 --- /dev/null +++ b/packages/cli-module-new/package.json @@ -0,0 +1,39 @@ +{ + "name": "@backstage/cli-module-new", + "version": "0.1.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" + ], + "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:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/packages/cli/src/modules/new/commands/new.test.ts b/packages/cli-module-new/src/commands/new.test.ts similarity index 96% rename from packages/cli/src/modules/new/commands/new.test.ts rename to packages/cli-module-new/src/commands/new.test.ts index 463add855d..54663ab60d 100644 --- a/packages/cli/src/modules/new/commands/new.test.ts +++ b/packages/cli-module-new/src/commands/new.test.ts @@ -16,7 +16,7 @@ import { createNewPackage } from '../lib/createNewPackage'; import { default as newCommand } from './new'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; jest.mock('../lib/createNewPackage'); diff --git a/packages/cli/src/modules/new/commands/new.ts b/packages/cli-module-new/src/commands/new.ts similarity index 98% rename from packages/cli/src/modules/new/commands/new.ts rename to packages/cli-module-new/src/commands/new.ts index 296a3a321e..3d7fcdf4e3 100644 --- a/packages/cli/src/modules/new/commands/new.ts +++ b/packages/cli-module-new/src/commands/new.ts @@ -16,7 +16,7 @@ import { cli } from 'cleye'; import { createNewPackage } from '../lib/createNewPackage'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { for (const flag of ['skipInstall', 'npmRegistry', 'baseVersion']) { diff --git a/packages/cli/src/modules/new/index.ts b/packages/cli-module-new/src/index.ts similarity index 97% rename from packages/cli/src/modules/new/index.ts rename to packages/cli-module-new/src/index.ts index 2a08a71e97..c9bc322083 100644 --- a/packages/cli/src/modules/new/index.ts +++ b/packages/cli-module-new/src/index.ts @@ -15,7 +15,7 @@ */ import { createCliModule } from '@backstage/cli-node'; import { NotImplementedError } from '@backstage/errors'; -import packageJson from '../../../package.json'; +import packageJson from '../package.json'; export default createCliModule({ packageJson, 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 100% rename from packages/cli/src/modules/new/lib/codeowners/codeowners.ts rename to packages/cli-module-new/src/lib/codeowners/codeowners.ts 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 100% rename from packages/cli/src/modules/new/lib/execution/PortableTemplater.ts rename to packages/cli-module-new/src/lib/execution/PortableTemplater.ts 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 100% rename from packages/cli/src/modules/new/lib/execution/installNewPackage.ts rename to packages/cli-module-new/src/lib/execution/installNewPackage.ts 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 100% rename from packages/cli/src/modules/new/lib/execution/writeTemplateContents.test.ts rename to packages/cli-module-new/src/lib/execution/writeTemplateContents.test.ts diff --git a/packages/cli/src/modules/new/lib/execution/writeTemplateContents.ts b/packages/cli-module-new/src/lib/execution/writeTemplateContents.ts similarity index 100% rename from packages/cli/src/modules/new/lib/execution/writeTemplateContents.ts rename to packages/cli-module-new/src/lib/execution/writeTemplateContents.ts 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 100% rename from packages/cli/src/modules/new/lib/preparation/collectPortableTemplateInput.ts rename to packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.ts 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 100% rename from packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.ts rename to packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts 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 100% rename from packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.ts rename to packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts 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/modules/new/lib/version.test.ts b/packages/cli-module-new/src/lib/version.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/version.test.ts rename to packages/cli-module-new/src/lib/version.test.ts diff --git a/packages/cli/src/modules/new/lib/version.ts b/packages/cli-module-new/src/lib/version.ts similarity index 63% rename from packages/cli/src/modules/new/lib/version.ts rename to packages/cli-module-new/src/lib/version.ts index 0849e8a9e8..57c5646a14 100644 --- a/packages/cli/src/modules/new/lib/version.ts +++ b/packages/cli-module-new/src/lib/version.ts @@ -30,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 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'; +type Pkg = { version: string }; +const v = (path: string) => (require(path) as Pkg).version; +const backendPluginApi = v('../../../backend-plugin-api/package.json'); +const backendTestUtils = v('../../../backend-test-utils/package.json'); +const catalogClient = v('../../../catalog-client/package.json'); +const cli = v('../../../cli/package.json'); +const config = v('../../../config/package.json'); +const coreAppApi = v('../../../core-app-api/package.json'); +const coreComponents = v('../../../core-components/package.json'); +const corePluginApi = v('../../../core-plugin-api/package.json'); +const devUtils = v('../../../dev-utils/package.json'); +const errors = v('../../../errors/package.json'); +const frontendDefaults = v('../../../frontend-defaults/package.json'); +const frontendPluginApi = v('../../../frontend-plugin-api/package.json'); +const frontendTestUtils = v('../../../frontend-test-utils/package.json'); +const testUtils = v('../../../test-utils/package.json'); +const scaffolderNode = v('../../../../plugins/scaffolder-node/package.json'); +const scaffolderNodeTestUtils = v('../../../../plugins/scaffolder-node-test-utils/package.json'); +const authBackend = v('../../../../plugins/auth-backend/package.json'); +const authBackendModuleGuestProvider = v('../../../../plugins/auth-backend-module-guest-provider/package.json'); +const catalogNode = v('../../../../plugins/catalog-node/package.json'); +const theme = v('../../../theme/package.json'); +const types = v('../../../types/package.json'); +const backendDefaults = v('../../../backend-defaults/package.json'); export const packageVersions: Record = { '@backstage/backend-defaults': backendDefaults, diff --git a/packages/cli-module-test-jest/.eslintrc.js b/packages/cli-module-test-jest/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-test-jest/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 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/package.json b/packages/cli-module-test-jest/package.json new file mode 100644 index 0000000000..7df933dee9 --- /dev/null +++ b/packages/cli-module-test-jest/package.json @@ -0,0 +1,39 @@ +{ + "name": "@backstage/cli-module-test-jest", + "version": "0.1.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" + ], + "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:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} 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 94% rename from packages/cli/src/modules/test/commands/package/test.ts rename to packages/cli-module-test-jest/src/commands/package/test.ts index b57ae909be..035db8272f 100644 --- a/packages/cli/src/modules/test/commands/package/test.ts +++ b/packages/cli-module-test-jest/src/commands/package/test.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { runCheck, findOwnPaths } from '@backstage/cli-common'; -import type { CliCommandContext } from '../../../../wiring/types'; +import { runCheck } from '@backstage/cli-common'; +import type { CliCommandContext } from '@backstage/cli-node'; function includesAnyOf(hayStack: string[], ...needles: string[]) { for (const needle of needles) { @@ -30,7 +30,7 @@ export default async ({ args }: CliCommandContext) => { // Only include our config if caller isn't passing their own config if (!includesAnyOf(args, '-c', '--config')) { /* eslint-disable-next-line no-restricted-syntax */ - args.push('--config', findOwnPaths(__dirname).resolve('config/jest.js')); + args.push('--config', require.resolve('@backstage/cli/config/jest')); } if (!includesAnyOf(args, '--no-passWithNoTests', '--passWithNoTests=false')) { diff --git a/packages/cli/src/modules/test/commands/repo/test.test.ts b/packages/cli-module-test-jest/src/commands/repo/test.test.ts similarity index 100% rename from packages/cli/src/modules/test/commands/repo/test.test.ts rename to packages/cli-module-test-jest/src/commands/repo/test.test.ts 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 98% rename from packages/cli/src/modules/test/commands/repo/test.ts rename to packages/cli-module-test-jest/src/commands/repo/test.ts index adcaf99dfd..e45df2bcfc 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli-module-test-jest/src/commands/repo/test.ts @@ -28,10 +28,9 @@ import { runCheck, runOutput, targetPaths, - findOwnPaths, isChildPath, } from '@backstage/cli-common'; -import type { CliCommandContext } from '../../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; type JestProject = { displayName: string; @@ -182,7 +181,7 @@ export default async ({ args, info }: CliCommandContext) => { // Only include our config if caller isn't passing their own config if (!hasFlags('-c', '--config')) { /* eslint-disable-next-line no-restricted-syntax */ - args.push('--config', findOwnPaths(__dirname).resolve('config/jest.js')); + args.push('--config', require.resolve('@backstage/cli/config/jest')); } if (!hasFlags('--passWithNoTests')) { diff --git a/packages/cli/src/modules/test/index.ts b/packages/cli-module-test-jest/src/index.ts similarity index 96% rename from packages/cli/src/modules/test/index.ts rename to packages/cli-module-test-jest/src/index.ts index f10e13828f..b2060fa89e 100644 --- a/packages/cli/src/modules/test/index.ts +++ b/packages/cli-module-test-jest/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createCliModule } from '@backstage/cli-node'; -import packageJson from '../../../package.json'; +import packageJson from '../package.json'; export default createCliModule({ packageJson, 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/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/package.json b/packages/cli-module-translations/package.json new file mode 100644 index 0000000000..8818ff32d4 --- /dev/null +++ b/packages/cli-module-translations/package.json @@ -0,0 +1,39 @@ +{ + "name": "@backstage/cli-module-translations", + "version": "0.1.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" + ], + "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:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/packages/cli/src/modules/translations/commands/export.ts b/packages/cli-module-translations/src/commands/export.ts similarity index 98% rename from packages/cli/src/modules/translations/commands/export.ts rename to packages/cli-module-translations/src/commands/export.ts index ca496ad859..a1d2eb2dae 100644 --- a/packages/cli/src/modules/translations/commands/export.ts +++ b/packages/cli-module-translations/src/commands/export.ts @@ -33,7 +33,7 @@ import { formatMessagePath, validatePattern, } from '../lib/messageFilePath'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { const { diff --git a/packages/cli/src/modules/translations/commands/import.ts b/packages/cli-module-translations/src/commands/import.ts similarity index 99% rename from packages/cli/src/modules/translations/commands/import.ts rename to packages/cli-module-translations/src/commands/import.ts index 0e27321da5..35a071c4fb 100644 --- a/packages/cli/src/modules/translations/commands/import.ts +++ b/packages/cli-module-translations/src/commands/import.ts @@ -28,7 +28,7 @@ import { createMessagePathParser, formatMessagePath, } from '../lib/messageFilePath'; -import type { CliCommandContext } from '../../../wiring/types'; +import type { CliCommandContext } from '@backstage/cli-node'; interface ManifestRefEntry { package: string; diff --git a/packages/cli/src/modules/translations/index.ts b/packages/cli-module-translations/src/index.ts similarity index 96% rename from packages/cli/src/modules/translations/index.ts rename to packages/cli-module-translations/src/index.ts index 7654c2e0b6..e075dc61ae 100644 --- a/packages/cli/src/modules/translations/index.ts +++ b/packages/cli-module-translations/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createCliModule } from '@backstage/cli-node'; -import packageJson from '../../../package.json'; +import packageJson from '../package.json'; export default createCliModule({ packageJson, diff --git a/packages/cli/src/modules/translations/lib/discoverPackages.ts b/packages/cli-module-translations/src/lib/discoverPackages.ts similarity index 100% rename from packages/cli/src/modules/translations/lib/discoverPackages.ts rename to packages/cli-module-translations/src/lib/discoverPackages.ts diff --git a/packages/cli/src/modules/translations/lib/extractTranslations.test.ts b/packages/cli-module-translations/src/lib/extractTranslations.test.ts similarity index 100% rename from packages/cli/src/modules/translations/lib/extractTranslations.test.ts rename to packages/cli-module-translations/src/lib/extractTranslations.test.ts diff --git a/packages/cli/src/modules/translations/lib/extractTranslations.ts b/packages/cli-module-translations/src/lib/extractTranslations.ts similarity index 100% rename from packages/cli/src/modules/translations/lib/extractTranslations.ts rename to packages/cli-module-translations/src/lib/extractTranslations.ts diff --git a/packages/cli/src/modules/translations/lib/messageFilePath.test.ts b/packages/cli-module-translations/src/lib/messageFilePath.test.ts similarity index 100% rename from packages/cli/src/modules/translations/lib/messageFilePath.test.ts rename to packages/cli-module-translations/src/lib/messageFilePath.test.ts diff --git a/packages/cli/src/modules/translations/lib/messageFilePath.ts b/packages/cli-module-translations/src/lib/messageFilePath.ts similarity index 100% rename from packages/cli/src/modules/translations/lib/messageFilePath.ts rename to packages/cli-module-translations/src/lib/messageFilePath.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 8e03c60d0f..189fd8faaa 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,6 +49,17 @@ "dependencies": { "@backstage/catalog-model": "workspace:^", "@backstage/cli-common": "workspace:^", + "@backstage/cli-module-auth": "workspace:^", + "@backstage/cli-module-build": "workspace:^", + "@backstage/cli-module-config": "workspace:^", + "@backstage/cli-module-create-github-app": "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:^", "@backstage/cli-node": "workspace:^", "@backstage/config": "workspace:^", "@backstage/config-loader": "workspace:^", diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 14da5e791e..8773c3b112 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -18,16 +18,16 @@ import { CliInitializer } from './wiring/CliInitializer'; (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')); - initializer.add(import('./modules/translations')); - initializer.add(import('./modules/auth')); + initializer.add(import('@backstage/cli-module-build')); + initializer.add(import('@backstage/cli-module-config')); + initializer.add(import('@backstage/cli-module-create-github-app')); + initializer.add(import('@backstage/cli-module-info')); + initializer.add(import('@backstage/cli-module-lint')); + initializer.add(import('@backstage/cli-module-maintenance')); + initializer.add(import('@backstage/cli-module-migrate')); + initializer.add(import('@backstage/cli-module-new')); + initializer.add(import('@backstage/cli-module-test-jest')); + initializer.add(import('@backstage/cli-module-translations')); + initializer.add(import('@backstage/cli-module-auth')); await initializer.run(); })(); diff --git a/yarn.lock b/yarn.lock index 7465a75f4d..98d39db148 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2801,6 +2801,221 @@ __metadata: 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" + proper-lockfile: "npm:^4.1.2" + yaml: "npm:^2.0.0" + zod: "npm:^3.25.76" + 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.2" + "@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" + "@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" + chalk: "npm:^4.0.0" + chokidar: "npm:^3.5.3" + cleye: "npm:^2.3.0" + cross-spawn: "npm:^7.0.6" + ctrlc-windows: "npm:^2.1.0" + esbuild-loader: "npm:^4.4.2" + eslint-rspack-plugin: "npm:^4.2.1" + eslint-webpack-plugin: "npm:^5.0.3" + fork-ts-checker-webpack-plugin: "npm:^9.1.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.10.1" + 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" + react-dev-utils: "npm:^12.0.0-next.60" + rollup: "npm:^4.59.0" + 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" + tar: "npm:^7.5.6" + ts-checker-rspack-plugin: "npm:^1.1.5" + ts-morph: "npm:^24.0.0" + webpack: "npm:^5.105.4" + webpack-dev-server: "npm:^5.2.3" + yn: "npm:^4.0.0" + 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" + languageName: unknown + linkType: soft + +"@backstage/cli-module-create-github-app@workspace:^, @backstage/cli-module-create-github-app@workspace:packages/cli-module-create-github-app": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-create-github-app@workspace:packages/cli-module-create-github-app" + 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" + 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" + 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" + fs-extra: "npm:^11.2.0" + globby: "npm:^11.0.0" + shell-quote: "npm:^1.8.1" + 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" + 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/test-utils": "workspace:^" + "@manypkg/get-packages": "npm:^1.1.3" + "@types/fs-extra": "npm:^11.0.0" + cleye: "npm:^2.3.0" + fs-extra: "npm:^11.2.0" + msw: "npm:^1.0.0" + 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/cli": "workspace:^" + "@backstage/cli-node": "workspace:^" + 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-node": "workspace:^" + 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-node": "workspace:^" + 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" @@ -2831,6 +3046,17 @@ __metadata: "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli-common": "workspace:^" + "@backstage/cli-module-auth": "workspace:^" + "@backstage/cli-module-build": "workspace:^" + "@backstage/cli-module-config": "workspace:^" + "@backstage/cli-module-create-github-app": "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:^" "@backstage/cli-node": "workspace:^" "@backstage/config": "workspace:^" "@backstage/config-loader": "workspace:^" @@ -14337,144 +14563,144 @@ __metadata: languageName: node linkType: hard -"@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" +"@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" conditions: os=android & cpu=arm languageName: node linkType: hard -"@oxc-resolver/binding-android-arm64@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-android-arm64@npm:11.19.1" +"@oxc-resolver/binding-android-arm64@npm:11.16.3": + version: 11.16.3 + resolution: "@oxc-resolver/binding-android-arm64@npm:11.16.3" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-darwin-arm64@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.19.1" +"@oxc-resolver/binding-darwin-arm64@npm:11.16.3": + version: 11.16.3 + resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.16.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-darwin-x64@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-darwin-x64@npm:11.19.1" +"@oxc-resolver/binding-darwin-x64@npm:11.16.3": + version: 11.16.3 + resolution: "@oxc-resolver/binding-darwin-x64@npm:11.16.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxc-resolver/binding-freebsd-x64@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-freebsd-x64@npm:11.19.1" +"@oxc-resolver/binding-freebsd-x64@npm:11.16.3": + version: 11.16.3 + resolution: "@oxc-resolver/binding-freebsd-x64@npm:11.16.3" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@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" +"@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" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@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" +"@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" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@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" +"@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" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@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" +"@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" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@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" +"@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" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@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" +"@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" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@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" +"@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" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@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" +"@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" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@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" +"@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" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@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" +"@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" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxc-resolver/binding-openharmony-arm64@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-openharmony-arm64@npm:11.19.1" +"@oxc-resolver/binding-openharmony-arm64@npm:11.16.3": + version: 11.16.3 + resolution: "@oxc-resolver/binding-openharmony-arm64@npm:11.16.3" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-wasm32-wasi@npm:11.19.1": - version: 11.19.1 - resolution: "@oxc-resolver/binding-wasm32-wasi@npm:11.19.1" +"@oxc-resolver/binding-wasm32-wasi@npm:11.16.3": + version: 11.16.3 + resolution: "@oxc-resolver/binding-wasm32-wasi@npm:11.16.3" dependencies: "@napi-rs/wasm-runtime": "npm:^1.1.1" conditions: cpu=wasm32 languageName: node linkType: hard -"@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" +"@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" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@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" +"@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" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@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" +"@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" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -14658,7 +14884,7 @@ __metadata: languageName: node linkType: hard -"@pmmmwh/react-refresh-webpack-plugin@npm:^0.6.0": +"@pmmmwh/react-refresh-webpack-plugin@npm:^0.6.0, @pmmmwh/react-refresh-webpack-plugin@npm:^0.6.2": version: 0.6.2 resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.6.2" dependencies: @@ -17287,156 +17513,177 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.53.3" +"@rollup/rollup-android-arm-eabi@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.59.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-android-arm64@npm:4.53.3" +"@rollup/rollup-android-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-android-arm64@npm:4.59.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-darwin-arm64@npm:4.53.3" +"@rollup/rollup-darwin-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.59.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-darwin-x64@npm:4.53.3" +"@rollup/rollup-darwin-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.59.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.53.3" +"@rollup/rollup-freebsd-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.59.0" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-freebsd-x64@npm:4.53.3" +"@rollup/rollup-freebsd-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.59.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.53.3" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.53.3" +"@rollup/rollup-linux-arm-musleabihf@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.59.0" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.53.3" +"@rollup/rollup-linux-arm64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.59.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.53.3" +"@rollup/rollup-linux-arm64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.59.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loong64-gnu@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.53.3" +"@rollup/rollup-linux-loong64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.59.0" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-gnu@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.53.3" +"@rollup/rollup-linux-loong64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.59.0" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.59.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.53.3" +"@rollup/rollup-linux-ppc64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.59.0" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.59.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.53.3" +"@rollup/rollup-linux-riscv64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.59.0" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.53.3" +"@rollup/rollup-linux-s390x-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.59.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.53.3" +"@rollup/rollup-linux-x64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.59.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.53.3" +"@rollup/rollup-linux-x64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.59.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-openharmony-arm64@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-openharmony-arm64@npm:4.53.3" +"@rollup/rollup-openbsd-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-openbsd-x64@npm:4.59.0" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.59.0" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.53.3" +"@rollup/rollup-win32-arm64-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.59.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.53.3" +"@rollup/rollup-win32-ia32-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.59.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-gnu@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-win32-x64-gnu@npm:4.53.3" +"@rollup/rollup-win32-x64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.59.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.53.3": - version: 4.53.3 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.53.3" +"@rollup/rollup-win32-x64-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.59.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -18965,8 +19212,8 @@ __metadata: linkType: hard "@snyk/dep-graph@npm:^2.12.0": - version: 2.14.0 - resolution: "@snyk/dep-graph@npm:2.14.0" + version: 2.16.3 + resolution: "@snyk/dep-graph@npm:2.16.3" dependencies: event-loop-spinner: "npm:^2.1.0" lodash.clone: "npm:^4.5.0" @@ -18987,7 +19234,7 @@ __metadata: packageurl-js: "npm:2.0.1" semver: "npm:^7.0.0" tslib: "npm:^2" - checksum: 10/e51882a2e566030a135d2fff477afbc7e4f8a14946089598fe56dd1e9b1b65fffe1ef8b3c8e2ff42629fc01d79b6d5dfeca1c505413731866c5b730728aee0a4 + checksum: 10/609c7946d0f64d4df4e68a3b5e2a61e5739f8b5dc57b42036f1651694fa1cc8d1537bb0e4a9a437a9fcd08c9b5db9d313c4c59a7e1267fb42993a9f41e78e2c6 languageName: node linkType: hard @@ -21185,7 +21432,17 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:*, @types/eslint@npm:^8.56.10": +"@types/eslint@npm:*, @types/eslint@npm:^9.6.1": + version: 9.6.1 + resolution: "@types/eslint@npm:9.6.1" + dependencies: + "@types/estree": "npm:*" + "@types/json-schema": "npm:*" + checksum: 10/719fcd255760168a43d0e306ef87548e1e15bffe361d5f4022b0f266575637acc0ecb85604ac97879ee8ae83c6a6d0613b0ed31d0209ddf22a0fe6d608fc56fe + languageName: node + linkType: hard + +"@types/eslint@npm:^8.56.10": version: 8.56.12 resolution: "@types/eslint@npm:8.56.12" dependencies: @@ -22842,9 +23099,9 @@ __metadata: languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.25.8": - version: 4.25.8 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.25.8" +"@uiw/codemirror-extensions-basic-setup@npm:4.25.4": + version: 4.25.4 + resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.25.4" dependencies: "@codemirror/autocomplete": "npm:^6.0.0" "@codemirror/commands": "npm:^6.0.0" @@ -22861,19 +23118,19 @@ __metadata: "@codemirror/search": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: 10/a8d83465f9f3393b6e95d98ae7f3616ad57f819bce64224831d3db19647524538fc013973074a63551afa69daad9a8ab05f2e5c7441db7e30e722495d7e991d3 + checksum: 10/8fa523af7cb4ea36db5263221e5a34ecbf18e57ee5c57fbb46f7bfd08ff08523b9fe2c5ae376117d98edaa1a904d1a2966123710e0d866a78ec2d1359955773c languageName: node linkType: hard "@uiw/react-codemirror@npm:^4.9.3": - version: 4.25.8 - resolution: "@uiw/react-codemirror@npm:4.25.8" + version: 4.25.4 + resolution: "@uiw/react-codemirror@npm:4.25.4" 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.8" + "@uiw/codemirror-extensions-basic-setup": "npm:4.25.4" codemirror: "npm:^6.0.0" peerDependencies: "@babel/runtime": ">=7.11.0" @@ -22883,7 +23140,7 @@ __metadata: codemirror: ">=6.0.0" react: ">=17.0.0" react-dom: ">=17.0.0" - checksum: 10/8c974e22dad1ad6231f33f7db42cd5b68caaf9bea545539b06b8a89dda3427eebadf47c8f48ee0d74cdf5a25000a8fcc02bac9fe560b624955eedf1f9bb47a85 + checksum: 10/679966cfa5cc2e7d5b7c8b40060018bebfc3a2d66492672c64e659e1c719cedd7a10ed7fb3aa014d77c42927581c4ebdf6760ab56568225781798b35dc13b8c6 languageName: node linkType: hard @@ -24652,7 +24909,14 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1, ansi-styles@npm:^6.2.3": +"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 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.2.3": version: 6.2.3 resolution: "ansi-styles@npm:6.2.3" checksum: 10/c49dad7639f3e48859bd51824c93b9eb0db628afc243c51c3dd2410c4a15ede1a83881c6c7341aa2b159c4f90c11befb38f2ba848c07c66c9f9de4bcd7cb9f30 @@ -29955,7 +30219,7 @@ __metadata: languageName: node linkType: hard -"esbuild-loader@npm:^4.0.0": +"esbuild-loader@npm:^4.0.0, esbuild-loader@npm:^4.4.2": version: 4.4.2 resolution: "esbuild-loader@npm:4.4.2" dependencies: @@ -30430,6 +30694,23 @@ __metadata: languageName: node linkType: hard +"eslint-webpack-plugin@npm:^5.0.3": + version: 5.0.3 + resolution: "eslint-webpack-plugin@npm:5.0.3" + dependencies: + "@types/eslint": "npm:^9.6.1" + flatted: "npm:^3.3.3" + jest-worker: "npm:^29.7.0" + micromatch: "npm:^4.0.8" + normalize-path: "npm:^3.0.0" + schema-utils: "npm:^4.3.2" + peerDependencies: + eslint: ^8.0.0 || ^9.0.0 + webpack: ^5.0.0 + checksum: 10/fbf86752ad9493ea70ef025a044c1437e105be51c430762ceac9ae930f4e67c9027d7b1b848969fb3abd285e32ce0fd04382d6e8326958be6b1068415bdd38ab + languageName: node + linkType: hard + "eslint@npm:^8.33.0, eslint@npm:^8.6.0": version: 8.57.1 resolution: "eslint@npm:8.57.1" @@ -31012,13 +31293,13 @@ __metadata: linkType: hard "express-rate-limit@npm:^8.2.1, express-rate-limit@npm:^8.2.2": - version: 8.3.1 - resolution: "express-rate-limit@npm:8.3.1" + version: 8.3.0 + resolution: "express-rate-limit@npm:8.3.0" dependencies: ip-address: "npm:10.1.0" peerDependencies: express: ">= 4.11" - checksum: 10/dd97bfc48c01a6d4c5433203232b5e7a1e55e21322bde49033e5f8c4339584fe671a94096144a0810f4ea21dcec8aaaf15823109627e609f8ed1bc5912a345cf + checksum: 10/e896a66fecc10639e65873186fdfb71f19d6af650220eb7ea5450725215c3eed8dc6ddcfa1e68a9db8c9facc3326fbc281512ad3ccd8f107f42a2466ce12c18c languageName: node linkType: hard @@ -31687,7 +31968,7 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^3.1.0, flatted@npm:^3.2.7, flatted@npm:^3.3.4": +"flatted@npm:^3.1.0, flatted@npm:^3.2.7, flatted@npm:^3.3.3, flatted@npm:^3.3.4": version: 3.4.1 resolution: "flatted@npm:3.4.1" checksum: 10/39a308e2ef82d2d8c80ebc74b67d4ff3f668be168137b649440b6735eb9c115d1e0c13ab0d9958b3d2ea9c85087ab7e14c14aa6f625a22b2916d89bbd91bc4a0 @@ -31787,7 +32068,7 @@ __metadata: languageName: node linkType: hard -"fork-ts-checker-webpack-plugin@npm:^9.0.0": +"fork-ts-checker-webpack-plugin@npm:^9.0.0, fork-ts-checker-webpack-plugin@npm:^9.1.0": version: 9.1.0 resolution: "fork-ts-checker-webpack-plugin@npm:9.1.0" dependencies: @@ -32290,7 +32571,14 @@ __metadata: languageName: node linkType: hard -"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.1, get-east-asian-width@npm:^1.5.0": +"get-east-asian-width@npm:^1.0.0": + version: 1.2.0 + resolution: "get-east-asian-width@npm:1.2.0" + checksum: 10/c9b280e7c7c67fb89fa17e867c4a9d1c9f1321aba2a9ee27bff37fb6ca9552bccda328c70a80c1f83a0e39ba1b7e3427e60f47823402d19e7a41b83417ec047a + languageName: node + linkType: hard + +"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 @@ -32677,13 +32965,20 @@ __metadata: languageName: node linkType: hard -"globals@npm:^17.0.0, globals@npm:^17.3.0": +"globals@npm:^17.0.0": version: 17.4.0 resolution: "globals@npm:17.4.0" checksum: 10/ffad244617e94efcb3da72b7beefc941167c21316148ce378f322db7af72db06468f370e23224b3c7b17b5173a7c75b134e5e7b0949f2828519054a76892508d languageName: node linkType: hard +"globals@npm:^17.3.0": + version: 17.3.0 + resolution: "globals@npm:17.3.0" + checksum: 10/44ba2b7db93eb6a2531dfba09219845e21f2e724a4f400eb59518b180b7d5bcf7f65580530e3d3023d7dc2bdbacf5d265fd87c393f567deb9a2b0472b51c9d5e + languageName: node + linkType: hard + "globalthis@npm:^1.0.1, globalthis@npm:^1.0.3, globalthis@npm:^1.0.4": version: 1.0.4 resolution: "globalthis@npm:1.0.4" @@ -33456,9 +33751,9 @@ __metadata: linkType: hard "hono@npm:^4.11.4": - version: 4.12.7 - resolution: "hono@npm:4.12.7" - checksum: 10/fed37e612730491ba9456f8f68f1b8727a5298cd839fff9641a0b7a95b1e8567a05abb819d32621b40988f166b01140cf7d573c9218dee2741004f48e09564d5 + version: 4.12.5 + resolution: "hono@npm:4.12.5" + checksum: 10/3d03cf2885f7c75325869a178bc94291a17a656002b930dc91456cb177366f6f344c7c90c09707c1fcb4c6e40b1f33fac98f1622628061628cabadcf8cf6d3fd languageName: node linkType: hard @@ -34626,7 +34921,16 @@ __metadata: languageName: node linkType: hard -"is-fullwidth-code-point@npm:^5.0.0, is-fullwidth-code-point@npm:^5.1.0": +"is-fullwidth-code-point@npm:^5.0.0": + version: 5.0.0 + resolution: "is-fullwidth-code-point@npm:5.0.0" + dependencies: + get-east-asian-width: "npm:^1.0.0" + checksum: 10/8dfb2d2831b9e87983c136f5c335cd9d14c1402973e357a8ff057904612ed84b8cba196319fabedf9aefe4639e14fe3afe9d9966d1d006ebeb40fe1fed4babe5 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^5.1.0": version: 5.1.0 resolution: "is-fullwidth-code-point@npm:5.1.0" dependencies: @@ -36773,21 +37077,20 @@ __metadata: linkType: hard "knip@npm:^5.42.0": - version: 5.86.0 - resolution: "knip@npm:5.86.0" + version: 5.85.0 + resolution: "knip@npm:5.85.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.19.1" + oxc-resolver: "npm:^11.15.0" 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" @@ -36795,7 +37098,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10/1b8c94f62d2868b0c62499474f462dc09d57e10e73335c01caacff8e0c0b120f441e0881028db30b6a1c185f98a78e2fa727e8faa150bca40c41848263aee04f + checksum: 10/1b80127b24bad3822c2a128e08c30e33db2c716ccb3c1ee19a082d5f04c7d24f049213e15a6369d9449bf60564a86be16e8f986902ba7251bee17a4530c6c954 languageName: node linkType: hard @@ -37003,18 +37306,18 @@ __metadata: linkType: hard "lint-staged@npm:^16.0.0": - version: 16.3.3 - resolution: "lint-staged@npm:16.3.3" + version: 16.4.0 + resolution: "lint-staged@npm:16.4.0" dependencies: commander: "npm:^14.0.3" listr2: "npm:^9.0.5" - micromatch: "npm:^4.0.8" + picomatch: "npm:^4.0.3" string-argv: "npm:^0.3.2" - tinyexec: "npm:^1.0.2" + tinyexec: "npm:^1.0.4" yaml: "npm:^2.8.2" bin: lint-staged: bin/lint-staged.js - checksum: 10/64b20a0aa8710a89db49b4d4384f44e471de8544133cd9f80b1ebeac148144e43b1553aea2b34bf530fd32b554374ee8128c9332033ba138f1a9685964de1cd3 + checksum: 10/eb7aa0d43e321bbf282ce0c693a3db762aa9b8e5bf29419a49c8877a247cd3a14cf8d519f914f8c8dcd2aea73ac83c0bb44fadb97a30004219e060a780dda74e languageName: node linkType: hard @@ -38825,15 +39128,15 @@ __metadata: languageName: node linkType: hard -"mini-css-extract-plugin@npm:^2.4.2": - version: 2.10.0 - resolution: "mini-css-extract-plugin@npm:2.10.0" +"mini-css-extract-plugin@npm:^2.10.1, mini-css-extract-plugin@npm:^2.4.2": + 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 @@ -40784,30 +41087,30 @@ __metadata: languageName: node linkType: hard -"oxc-resolver@npm:^11.19.1": - version: 11.19.1 - resolution: "oxc-resolver@npm:11.19.1" +"oxc-resolver@npm:^11.15.0": + version: 11.16.3 + resolution: "oxc-resolver@npm:11.16.3" dependencies: - "@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" + "@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" dependenciesMeta: "@oxc-resolver/binding-android-arm-eabi": optional: true @@ -40849,7 +41152,7 @@ __metadata: optional: true "@oxc-resolver/binding-win32-x64-msvc": optional: true - checksum: 10/a6c8fdb2ef4bf9bb84f28e58685457de427d31f74373c0fbd6d1106010cab33027fa3b4336b1b86d0df0a089cd73a6060b730b1b24974d56c59f6fa29c559f9d + checksum: 10/2d0da140e34a74347d03a233488ad0284bbb252a6d250b7ca78d5a431d04d72b8275d896b8fb0d03846f221bcbcb2395739187d2645bf84d1d1332b74b7720be languageName: node linkType: hard @@ -42463,13 +42766,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.8 - resolution: "postcss@npm:8.5.8" + version: 8.5.6 + resolution: "postcss@npm:8.5.6" dependencies: nanoid: "npm:^3.3.11" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10/cbacbfd7f767e2c820d4bf09a3a744834dd7d14f69ff08d1f57b1a7defce9ae5efcf31981890d9697a972a64e9965de677932ef28e4c8ba23a87aad45b82c459 + checksum: 10/9e4fbe97574091e9736d0e82a591e29aa100a0bf60276a926308f8c57249698935f35c5d2f4e80de778d0cbb8dcffab4f383d85fd50c5649aca421c3df729b86 languageName: node linkType: hard @@ -45194,32 +45497,35 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.27.3, rollup@npm:^4.43.0": - version: 4.53.3 - resolution: "rollup@npm:4.53.3" +"rollup@npm:^4.27.3, rollup@npm:^4.43.0, rollup@npm:^4.59.0": + version: 4.59.0 + resolution: "rollup@npm:4.59.0" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.53.3" - "@rollup/rollup-android-arm64": "npm:4.53.3" - "@rollup/rollup-darwin-arm64": "npm:4.53.3" - "@rollup/rollup-darwin-x64": "npm:4.53.3" - "@rollup/rollup-freebsd-arm64": "npm:4.53.3" - "@rollup/rollup-freebsd-x64": "npm:4.53.3" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.53.3" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.53.3" - "@rollup/rollup-linux-arm64-gnu": "npm:4.53.3" - "@rollup/rollup-linux-arm64-musl": "npm:4.53.3" - "@rollup/rollup-linux-loong64-gnu": "npm:4.53.3" - "@rollup/rollup-linux-ppc64-gnu": "npm:4.53.3" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.53.3" - "@rollup/rollup-linux-riscv64-musl": "npm:4.53.3" - "@rollup/rollup-linux-s390x-gnu": "npm:4.53.3" - "@rollup/rollup-linux-x64-gnu": "npm:4.53.3" - "@rollup/rollup-linux-x64-musl": "npm:4.53.3" - "@rollup/rollup-openharmony-arm64": "npm:4.53.3" - "@rollup/rollup-win32-arm64-msvc": "npm:4.53.3" - "@rollup/rollup-win32-ia32-msvc": "npm:4.53.3" - "@rollup/rollup-win32-x64-gnu": "npm:4.53.3" - "@rollup/rollup-win32-x64-msvc": "npm:4.53.3" + "@rollup/rollup-android-arm-eabi": "npm:4.59.0" + "@rollup/rollup-android-arm64": "npm:4.59.0" + "@rollup/rollup-darwin-arm64": "npm:4.59.0" + "@rollup/rollup-darwin-x64": "npm:4.59.0" + "@rollup/rollup-freebsd-arm64": "npm:4.59.0" + "@rollup/rollup-freebsd-x64": "npm:4.59.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.59.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.59.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.59.0" + "@rollup/rollup-linux-loong64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-loong64-musl": "npm:4.59.0" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-ppc64-musl": "npm:4.59.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-riscv64-musl": "npm:4.59.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.59.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-x64-musl": "npm:4.59.0" + "@rollup/rollup-openbsd-x64": "npm:4.59.0" + "@rollup/rollup-openharmony-arm64": "npm:4.59.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.59.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.59.0" + "@rollup/rollup-win32-x64-gnu": "npm:4.59.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.59.0" "@types/estree": "npm:1.0.8" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -45245,8 +45551,12 @@ __metadata: optional: true "@rollup/rollup-linux-loong64-gnu": optional: true + "@rollup/rollup-linux-loong64-musl": + optional: true "@rollup/rollup-linux-ppc64-gnu": optional: true + "@rollup/rollup-linux-ppc64-musl": + optional: true "@rollup/rollup-linux-riscv64-gnu": optional: true "@rollup/rollup-linux-riscv64-musl": @@ -45257,6 +45567,8 @@ __metadata: optional: true "@rollup/rollup-linux-x64-musl": optional: true + "@rollup/rollup-openbsd-x64": + optional: true "@rollup/rollup-openharmony-arm64": optional: true "@rollup/rollup-win32-arm64-msvc": @@ -45271,7 +45583,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/e2eff82405061fa907f15dfbf742b1f5fb4b214495c00989bcdbe21da5fcb3f6dec3deabacec491300a53c99da409586cfc77bdf29b411fccb9089b72cd3728d + checksum: 10/728237932aad7022c0640cd126b9fe5285f2578099f22a0542229a17785320a6553b74582fa5977877541c1faf27de65ed2750bc89dbb55b525405244a46d9f1 languageName: node linkType: hard @@ -45614,7 +45926,7 @@ __metadata: languageName: node linkType: hard -"schema-utils@npm:^4.0.0, schema-utils@npm:^4.2.0, schema-utils@npm:^4.3.0, schema-utils@npm:^4.3.3": +"schema-utils@npm:^4.0.0, schema-utils@npm:^4.2.0, schema-utils@npm:^4.3.0, schema-utils@npm:^4.3.2, schema-utils@npm:^4.3.3": version: 4.3.3 resolution: "schema-utils@npm:4.3.3" dependencies: @@ -47724,15 +48036,15 @@ __metadata: linkType: hard "tar@npm:^7.4.3, tar@npm:^7.5.6": - version: 7.5.11 - resolution: "tar@npm:7.5.11" + version: 7.5.10 + resolution: "tar@npm:7.5.10" 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/fb2e77ee858a73936c68e066f4a602d428d6f812e6da0cc1e14a41f99498e4f7fd3535e355fa15157240a5538aa416026cfa6306bb0d1d1c1abf314b1f878e9a + checksum: 10/98ba6421a250b233c36a54f7441647bdfee1ed0b916cd57850259a3602154d996f5b8422f67ef5c8ce77f582ed938054775c2873fc7c901e0c7530ed50febc40 languageName: node linkType: hard @@ -48067,10 +48379,10 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^1.0.2": - version: 1.0.2 - resolution: "tinyexec@npm:1.0.2" - checksum: 10/cb709ed4240e873d3816e67f851d445f5676e0ae3a52931a60ff571d93d388da09108c8057b62351766133ee05ff3159dd56c3a0fbd39a5933c6639ce8771405 +"tinyexec@npm:^1.0.4": + version: 1.0.4 + resolution: "tinyexec@npm:1.0.4" + checksum: 10/ccebe4044eef6fa5050929df7862fda70b4fb700f15d94aef8ae6109b9d194dbc3a990125d99944fd25b90fe2115e1927f055b909a604c571a81b647ede5757a languageName: node linkType: hard @@ -49027,13 +49339,6 @@ __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" @@ -49091,9 +49396,9 @@ __metadata: linkType: hard "undici@npm:^7.2.3, undici@npm:^7.22.0": - version: 7.24.1 - resolution: "undici@npm:7.24.1" - checksum: 10/0af4ec2fc2ae50b1d0636895793ea2274a89a7cd445690089a1b957c4ac4a0336e48ab9ac48a4e8f5023d4b5eab56368c3f17c75d6b4c81b4281b674d1e60c9f + version: 7.22.0 + resolution: "undici@npm:7.22.0" + checksum: 10/a7a1813ba4b74c0d46cc8dd160386202c05699ffc487c5d882cf40e6d2435c8d6faff3b8f8675d09bd1ef0386e370675c26b59b9a8c8b3f17b9f82a42236a927 languageName: node linkType: hard @@ -50190,7 +50495,7 @@ __metadata: languageName: node linkType: hard -"webpack-dev-server@npm:^5.0.0": +"webpack-dev-server@npm:^5.0.0, webpack-dev-server@npm:^5.2.3": version: 5.2.3 resolution: "webpack-dev-server@npm:5.2.3" dependencies: @@ -50259,7 +50564,7 @@ __metadata: languageName: node linkType: hard -"webpack@npm:^5, webpack@npm:~5.105.0": +"webpack@npm:^5, webpack@npm:^5.105.4, webpack@npm:~5.105.0": version: 5.105.4 resolution: "webpack@npm:5.105.4" dependencies: @@ -50869,12 +51174,12 @@ __metadata: linkType: soft "yauzl@npm:^3.0.0": - version: 3.2.1 - resolution: "yauzl@npm:3.2.1" + version: 3.2.0 + resolution: "yauzl@npm:3.2.0" dependencies: buffer-crc32: "npm:~0.2.3" pend: "npm:~1.2.0" - checksum: 10/15dfae75fbfe59c6a1b7a2cb27a995cda0ee70549d32d6b19937e84897436170f169f6bbefc34b9e9beb9c9114a1b8a8a40e7687a907909a19681ebcbf35a1f3 + checksum: 10/a3cd2bfcf7590673bb35750f2a4e5107e3cc939d32d98a072c0673fe42329e390f471b4a53dbbd72512229099b18aa3b79e6ddb87a73b3a17446080c903a2c4b languageName: node linkType: hard From c95a130f52115d0c3cac875d4a786879b704f36c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 13 Mar 2026 15:40:15 +0100 Subject: [PATCH 111/188] Add runCliModule helper for standalone module execution Add a public `runCliModule` function to `@backstage/cli-node` that allows CLI module packages to be executed directly as standalone programs, without needing to be wired into a larger CLI host. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-node/package.json | 2 + packages/cli-node/report.api.md | 7 + packages/cli-node/src/cli-module/index.ts | 1 + .../cli-node/src/cli-module/runCliModule.ts | 232 ++++++++++++++++++ yarn.lock | 4 +- 5 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 packages/cli-node/src/cli-module/runCliModule.ts diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index 4fa5af208c..95322ad2f0 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -37,6 +37,8 @@ "@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", "semver": "^7.5.3", "yaml": "^2.0.0", diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index b979fff0b0..2fac82080b 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -253,6 +253,13 @@ export class PackageRoles { static getRoleInfo(role: string): PackageRoleInfo; } +// @public +export function runCliModule(options: { + module: CliModule; + name: string; + version?: string; +}): Promise; + // @public export function runConcurrentTasks( options: ConcurrentTasksOptions, diff --git a/packages/cli-node/src/cli-module/index.ts b/packages/cli-node/src/cli-module/index.ts index 646073a815..56324c3423 100644 --- a/packages/cli-node/src/cli-module/index.ts +++ b/packages/cli-node/src/cli-module/index.ts @@ -15,4 +15,5 @@ */ 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..84893f0cac --- /dev/null +++ b/packages/cli-node/src/cli-module/runCliModule.ts @@ -0,0 +1,232 @@ +/* + * 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, +} 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 isCommandHidden(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 => isCommandHidden(child)); +} + +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 { + if (!isError(error)) { + process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); + process.exit(1); + } + process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); + process.exit( + '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: isCommandHidden(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/yarn.lock b/yarn.lock index 98d39db148..d753c5d9cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3030,6 +3030,8 @@ __metadata: "@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" semver: "npm:^7.5.3" yaml: "npm:^2.0.0" @@ -27535,7 +27537,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^12.1.0": +"commander@npm:^12.0.0, commander@npm:^12.1.0": version: 12.1.0 resolution: "commander@npm:12.1.0" checksum: 10/cdaeb672d979816853a4eed7f1310a9319e8b976172485c2a6b437ed0db0a389a44cfb222bfbde772781efa9f215bdd1b936f80d6b249485b465c6cb906e1f93 From 401c1f7e24bdb9de33bbcfdbd5f2a38777f967d2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 14 Mar 2026 11:25:57 +0100 Subject: [PATCH 112/188] Resolve CLI version dynamically in info module The info command now tries to resolve the @backstage/cli version at runtime instead of using a hardcoded relative path. This allows the module to work standalone when the CLI package is not present. The module's own version is always reported as infoModuleVersion. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-info/src/commands/info.ts | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/cli-module-info/src/commands/info.ts b/packages/cli-module-info/src/commands/info.ts index dd9f892d27..e7307e18c2 100644 --- a/packages/cli-module-info/src/commands/info.ts +++ b/packages/cli-module-info/src/commands/info.ts @@ -15,9 +15,7 @@ */ import { cli } from 'cleye'; -const { version: cliVersion } = require('../../../package.json') as { - version: string; -}; +import { version as infoModuleVersion } from '../../package.json'; import os from 'node:os'; import { runOutput, targetPaths, findOwnPaths } from '@backstage/cli-common'; import { @@ -101,12 +99,27 @@ export default async ({ args, info }: CliCommandContext) => { } } - // 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, }; @@ -210,8 +223,11 @@ export default async ({ args, info }: CliCommandContext) => { 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 From 64a96d9d9f6363e4627e24e6df21f79d024f5a1b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 14 Mar 2026 11:47:47 +0100 Subject: [PATCH 113/188] Add bin entry points for standalone CLI module execution Each CLI module package now includes a bin script and cli.ts entry point, allowing modules to be executed directly via npx without being wired into the main @backstage/cli package. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-auth/bin/cli-module-auth | 33 +++++++++++++++++++ packages/cli-module-auth/package.json | 6 ++-- packages/cli-module-auth/src/cli.ts | 26 +++++++++++++++ .../cli-module-build/bin/cli-module-build | 33 +++++++++++++++++++ packages/cli-module-build/package.json | 6 ++-- packages/cli-module-build/src/cli.ts | 26 +++++++++++++++ .../cli-module-config/bin/cli-module-config | 33 +++++++++++++++++++ packages/cli-module-config/package.json | 6 ++-- packages/cli-module-config/src/cli.ts | 26 +++++++++++++++ .../bin/cli-module-create-github-app | 33 +++++++++++++++++++ .../cli-module-create-github-app/package.json | 6 ++-- .../cli-module-create-github-app/src/cli.ts | 26 +++++++++++++++ packages/cli-module-info/bin/cli-module-info | 33 +++++++++++++++++++ packages/cli-module-info/package.json | 6 ++-- packages/cli-module-info/src/cli.ts | 26 +++++++++++++++ packages/cli-module-lint/bin/cli-module-lint | 33 +++++++++++++++++++ packages/cli-module-lint/package.json | 6 ++-- packages/cli-module-lint/src/cli.ts | 26 +++++++++++++++ .../bin/cli-module-maintenance | 33 +++++++++++++++++++ packages/cli-module-maintenance/package.json | 6 ++-- packages/cli-module-maintenance/src/cli.ts | 26 +++++++++++++++ .../cli-module-migrate/bin/cli-module-migrate | 33 +++++++++++++++++++ packages/cli-module-migrate/package.json | 6 ++-- packages/cli-module-migrate/src/cli.ts | 26 +++++++++++++++ packages/cli-module-new/bin/cli-module-new | 33 +++++++++++++++++++ packages/cli-module-new/package.json | 6 ++-- packages/cli-module-new/src/cli.ts | 26 +++++++++++++++ .../bin/cli-module-test-jest | 33 +++++++++++++++++++ packages/cli-module-test-jest/package.json | 6 ++-- packages/cli-module-test-jest/src/cli.ts | 26 +++++++++++++++ .../bin/cli-module-translations | 33 +++++++++++++++++++ packages/cli-module-translations/package.json | 6 ++-- packages/cli-module-translations/src/cli.ts | 26 +++++++++++++++ yarn.lock | 22 +++++++++++++ 34 files changed, 715 insertions(+), 22 deletions(-) create mode 100755 packages/cli-module-auth/bin/cli-module-auth create mode 100644 packages/cli-module-auth/src/cli.ts create mode 100755 packages/cli-module-build/bin/cli-module-build create mode 100644 packages/cli-module-build/src/cli.ts create mode 100755 packages/cli-module-config/bin/cli-module-config create mode 100644 packages/cli-module-config/src/cli.ts create mode 100755 packages/cli-module-create-github-app/bin/cli-module-create-github-app create mode 100644 packages/cli-module-create-github-app/src/cli.ts create mode 100755 packages/cli-module-info/bin/cli-module-info create mode 100644 packages/cli-module-info/src/cli.ts create mode 100755 packages/cli-module-lint/bin/cli-module-lint create mode 100644 packages/cli-module-lint/src/cli.ts create mode 100755 packages/cli-module-maintenance/bin/cli-module-maintenance create mode 100644 packages/cli-module-maintenance/src/cli.ts create mode 100755 packages/cli-module-migrate/bin/cli-module-migrate create mode 100644 packages/cli-module-migrate/src/cli.ts create mode 100755 packages/cli-module-new/bin/cli-module-new create mode 100644 packages/cli-module-new/src/cli.ts create mode 100755 packages/cli-module-test-jest/bin/cli-module-test-jest create mode 100644 packages/cli-module-test-jest/src/cli.ts create mode 100755 packages/cli-module-translations/bin/cli-module-translations create mode 100644 packages/cli-module-translations/src/cli.ts diff --git a/packages/cli-module-auth/bin/cli-module-auth b/packages/cli-module-auth/bin/cli-module-auth new file mode 100755 index 0000000000..533974f6a9 --- /dev/null +++ b/packages/cli-module-auth/bin/cli-module-auth @@ -0,0 +1,33 @@ +#!/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) { + const { runCliModule } = require('@backstage/cli-node'); + const cliModule = require('..').default; + const pkg = require('../package.json'); + runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); +} else { + require('@backstage/cli/config/nodeTransform.cjs'); + require('../src/cli'); +} diff --git a/packages/cli-module-auth/package.json b/packages/cli-module-auth/package.json index d00b1010f6..c3f4b55d0e 100644 --- a/packages/cli-module-auth/package.json +++ b/packages/cli-module-auth/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "bin" ], "scripts": { "build": "backstage-cli package build", @@ -47,5 +48,6 @@ "@types/fs-extra": "^11.0.0", "@types/proper-lockfile": "^4", "cross-fetch": "^4.0.0" - } + }, + "bin": "bin/cli-module-auth" } diff --git a/packages/cli-module-auth/src/cli.ts b/packages/cli-module-auth/src/cli.ts new file mode 100644 index 0000000000..6d4b925cf1 --- /dev/null +++ b/packages/cli-module-auth/src/cli.ts @@ -0,0 +1,26 @@ +/* + * 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 { runCliModule } from '@backstage/cli-node'; +import cliModule from './index'; + +const pkg = require('../package.json') as { name: string; version: string }; + +runCliModule({ + module: cliModule, + name: pkg.name, + version: pkg.version, +}); diff --git a/packages/cli-module-build/bin/cli-module-build b/packages/cli-module-build/bin/cli-module-build new file mode 100755 index 0000000000..533974f6a9 --- /dev/null +++ b/packages/cli-module-build/bin/cli-module-build @@ -0,0 +1,33 @@ +#!/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) { + const { runCliModule } = require('@backstage/cli-node'); + const cliModule = require('..').default; + const pkg = require('../package.json'); + runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); +} else { + require('@backstage/cli/config/nodeTransform.cjs'); + require('../src/cli'); +} diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index 0a4849671c..dd59b6948f 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "bin" ], "scripts": { "build": "backstage-cli package build", @@ -88,5 +89,6 @@ "cross-spawn": "^7.0.6", "rollup": "^4.59.0", "ts-morph": "^24.0.0" - } + }, + "bin": "bin/cli-module-build" } diff --git a/packages/cli-module-build/src/cli.ts b/packages/cli-module-build/src/cli.ts new file mode 100644 index 0000000000..6d4b925cf1 --- /dev/null +++ b/packages/cli-module-build/src/cli.ts @@ -0,0 +1,26 @@ +/* + * 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 { runCliModule } from '@backstage/cli-node'; +import cliModule from './index'; + +const pkg = require('../package.json') as { name: string; version: string }; + +runCliModule({ + module: cliModule, + name: pkg.name, + version: pkg.version, +}); diff --git a/packages/cli-module-config/bin/cli-module-config b/packages/cli-module-config/bin/cli-module-config new file mode 100755 index 0000000000..533974f6a9 --- /dev/null +++ b/packages/cli-module-config/bin/cli-module-config @@ -0,0 +1,33 @@ +#!/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) { + const { runCliModule } = require('@backstage/cli-node'); + const cliModule = require('..').default; + const pkg = require('../package.json'); + runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); +} else { + require('@backstage/cli/config/nodeTransform.cjs'); + require('../src/cli'); +} diff --git a/packages/cli-module-config/package.json b/packages/cli-module-config/package.json index 841f40de35..bcf5059280 100644 --- a/packages/cli-module-config/package.json +++ b/packages/cli-module-config/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "bin" ], "scripts": { "build": "backstage-cli package build", @@ -46,5 +47,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^" - } + }, + "bin": "bin/cli-module-config" } diff --git a/packages/cli-module-config/src/cli.ts b/packages/cli-module-config/src/cli.ts new file mode 100644 index 0000000000..6d4b925cf1 --- /dev/null +++ b/packages/cli-module-config/src/cli.ts @@ -0,0 +1,26 @@ +/* + * 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 { runCliModule } from '@backstage/cli-node'; +import cliModule from './index'; + +const pkg = require('../package.json') as { name: string; version: string }; + +runCliModule({ + module: cliModule, + name: pkg.name, + version: pkg.version, +}); diff --git a/packages/cli-module-create-github-app/bin/cli-module-create-github-app b/packages/cli-module-create-github-app/bin/cli-module-create-github-app new file mode 100755 index 0000000000..533974f6a9 --- /dev/null +++ b/packages/cli-module-create-github-app/bin/cli-module-create-github-app @@ -0,0 +1,33 @@ +#!/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) { + const { runCliModule } = require('@backstage/cli-node'); + const cliModule = require('..').default; + const pkg = require('../package.json'); + runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); +} else { + require('@backstage/cli/config/nodeTransform.cjs'); + require('../src/cli'); +} diff --git a/packages/cli-module-create-github-app/package.json b/packages/cli-module-create-github-app/package.json index 878ac0b3f8..81daa72058 100644 --- a/packages/cli-module-create-github-app/package.json +++ b/packages/cli-module-create-github-app/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "bin" ], "scripts": { "build": "backstage-cli package build", @@ -46,5 +47,6 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/fs-extra": "^11.0.0" - } + }, + "bin": "bin/cli-module-create-github-app" } diff --git a/packages/cli-module-create-github-app/src/cli.ts b/packages/cli-module-create-github-app/src/cli.ts new file mode 100644 index 0000000000..6d4b925cf1 --- /dev/null +++ b/packages/cli-module-create-github-app/src/cli.ts @@ -0,0 +1,26 @@ +/* + * 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 { runCliModule } from '@backstage/cli-node'; +import cliModule from './index'; + +const pkg = require('../package.json') as { name: string; version: string }; + +runCliModule({ + module: cliModule, + name: pkg.name, + version: pkg.version, +}); diff --git a/packages/cli-module-info/bin/cli-module-info b/packages/cli-module-info/bin/cli-module-info new file mode 100755 index 0000000000..533974f6a9 --- /dev/null +++ b/packages/cli-module-info/bin/cli-module-info @@ -0,0 +1,33 @@ +#!/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) { + const { runCliModule } = require('@backstage/cli-node'); + const cliModule = require('..').default; + const pkg = require('../package.json'); + runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); +} else { + require('@backstage/cli/config/nodeTransform.cjs'); + require('../src/cli'); +} diff --git a/packages/cli-module-info/package.json b/packages/cli-module-info/package.json index 5e1dd894e9..50f79da6f0 100644 --- a/packages/cli-module-info/package.json +++ b/packages/cli-module-info/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "bin" ], "scripts": { "build": "backstage-cli package build", @@ -40,5 +41,6 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/fs-extra": "^11.0.0" - } + }, + "bin": "bin/cli-module-info" } diff --git a/packages/cli-module-info/src/cli.ts b/packages/cli-module-info/src/cli.ts new file mode 100644 index 0000000000..6d4b925cf1 --- /dev/null +++ b/packages/cli-module-info/src/cli.ts @@ -0,0 +1,26 @@ +/* + * 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 { runCliModule } from '@backstage/cli-node'; +import cliModule from './index'; + +const pkg = require('../package.json') as { name: string; version: string }; + +runCliModule({ + module: cliModule, + name: pkg.name, + version: pkg.version, +}); diff --git a/packages/cli-module-lint/bin/cli-module-lint b/packages/cli-module-lint/bin/cli-module-lint new file mode 100755 index 0000000000..533974f6a9 --- /dev/null +++ b/packages/cli-module-lint/bin/cli-module-lint @@ -0,0 +1,33 @@ +#!/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) { + const { runCliModule } = require('@backstage/cli-node'); + const cliModule = require('..').default; + const pkg = require('../package.json'); + runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); +} else { + require('@backstage/cli/config/nodeTransform.cjs'); + require('../src/cli'); +} diff --git a/packages/cli-module-lint/package.json b/packages/cli-module-lint/package.json index 40839a0046..50edbae5eb 100644 --- a/packages/cli-module-lint/package.json +++ b/packages/cli-module-lint/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "bin" ], "scripts": { "build": "backstage-cli package build", @@ -44,5 +45,6 @@ "@backstage/cli": "workspace:^", "@types/fs-extra": "^11.0.0", "@types/shell-quote": "^1.7.5" - } + }, + "bin": "bin/cli-module-lint" } diff --git a/packages/cli-module-lint/src/cli.ts b/packages/cli-module-lint/src/cli.ts new file mode 100644 index 0000000000..6d4b925cf1 --- /dev/null +++ b/packages/cli-module-lint/src/cli.ts @@ -0,0 +1,26 @@ +/* + * 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 { runCliModule } from '@backstage/cli-node'; +import cliModule from './index'; + +const pkg = require('../package.json') as { name: string; version: string }; + +runCliModule({ + module: cliModule, + name: pkg.name, + version: pkg.version, +}); diff --git a/packages/cli-module-maintenance/bin/cli-module-maintenance b/packages/cli-module-maintenance/bin/cli-module-maintenance new file mode 100755 index 0000000000..533974f6a9 --- /dev/null +++ b/packages/cli-module-maintenance/bin/cli-module-maintenance @@ -0,0 +1,33 @@ +#!/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) { + const { runCliModule } = require('@backstage/cli-node'); + const cliModule = require('..').default; + const pkg = require('../package.json'); + runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); +} else { + require('@backstage/cli/config/nodeTransform.cjs'); + require('../src/cli'); +} diff --git a/packages/cli-module-maintenance/package.json b/packages/cli-module-maintenance/package.json index 5d321f88f0..69888e502f 100644 --- a/packages/cli-module-maintenance/package.json +++ b/packages/cli-module-maintenance/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "bin" ], "scripts": { "build": "backstage-cli package build", @@ -41,5 +42,6 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/fs-extra": "^11.0.0" - } + }, + "bin": "bin/cli-module-maintenance" } diff --git a/packages/cli-module-maintenance/src/cli.ts b/packages/cli-module-maintenance/src/cli.ts new file mode 100644 index 0000000000..6d4b925cf1 --- /dev/null +++ b/packages/cli-module-maintenance/src/cli.ts @@ -0,0 +1,26 @@ +/* + * 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 { runCliModule } from '@backstage/cli-node'; +import cliModule from './index'; + +const pkg = require('../package.json') as { name: string; version: string }; + +runCliModule({ + module: cliModule, + name: pkg.name, + version: pkg.version, +}); diff --git a/packages/cli-module-migrate/bin/cli-module-migrate b/packages/cli-module-migrate/bin/cli-module-migrate new file mode 100755 index 0000000000..533974f6a9 --- /dev/null +++ b/packages/cli-module-migrate/bin/cli-module-migrate @@ -0,0 +1,33 @@ +#!/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) { + const { runCliModule } = require('@backstage/cli-node'); + const cliModule = require('..').default; + const pkg = require('../package.json'); + runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); +} else { + require('@backstage/cli/config/nodeTransform.cjs'); + require('../src/cli'); +} diff --git a/packages/cli-module-migrate/package.json b/packages/cli-module-migrate/package.json index 660b85dff2..49faa42261 100644 --- a/packages/cli-module-migrate/package.json +++ b/packages/cli-module-migrate/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "bin" ], "scripts": { "build": "backstage-cli package build", @@ -44,5 +45,6 @@ "@backstage/test-utils": "workspace:^", "@types/fs-extra": "^11.0.0", "msw": "^1.0.0" - } + }, + "bin": "bin/cli-module-migrate" } diff --git a/packages/cli-module-migrate/src/cli.ts b/packages/cli-module-migrate/src/cli.ts new file mode 100644 index 0000000000..6d4b925cf1 --- /dev/null +++ b/packages/cli-module-migrate/src/cli.ts @@ -0,0 +1,26 @@ +/* + * 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 { runCliModule } from '@backstage/cli-node'; +import cliModule from './index'; + +const pkg = require('../package.json') as { name: string; version: string }; + +runCliModule({ + module: cliModule, + name: pkg.name, + version: pkg.version, +}); diff --git a/packages/cli-module-new/bin/cli-module-new b/packages/cli-module-new/bin/cli-module-new new file mode 100755 index 0000000000..533974f6a9 --- /dev/null +++ b/packages/cli-module-new/bin/cli-module-new @@ -0,0 +1,33 @@ +#!/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) { + const { runCliModule } = require('@backstage/cli-node'); + const cliModule = require('..').default; + const pkg = require('../package.json'); + runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); +} else { + require('@backstage/cli/config/nodeTransform.cjs'); + require('../src/cli'); +} diff --git a/packages/cli-module-new/package.json b/packages/cli-module-new/package.json index 5a04becd22..a49ccaa6d7 100644 --- a/packages/cli-module-new/package.json +++ b/packages/cli-module-new/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "bin" ], "scripts": { "build": "backstage-cli package build", @@ -35,5 +36,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^" - } + }, + "bin": "bin/cli-module-new" } diff --git a/packages/cli-module-new/src/cli.ts b/packages/cli-module-new/src/cli.ts new file mode 100644 index 0000000000..6d4b925cf1 --- /dev/null +++ b/packages/cli-module-new/src/cli.ts @@ -0,0 +1,26 @@ +/* + * 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 { runCliModule } from '@backstage/cli-node'; +import cliModule from './index'; + +const pkg = require('../package.json') as { name: string; version: string }; + +runCliModule({ + module: cliModule, + name: pkg.name, + version: pkg.version, +}); diff --git a/packages/cli-module-test-jest/bin/cli-module-test-jest b/packages/cli-module-test-jest/bin/cli-module-test-jest new file mode 100755 index 0000000000..533974f6a9 --- /dev/null +++ b/packages/cli-module-test-jest/bin/cli-module-test-jest @@ -0,0 +1,33 @@ +#!/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) { + const { runCliModule } = require('@backstage/cli-node'); + const cliModule = require('..').default; + const pkg = require('../package.json'); + runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); +} else { + require('@backstage/cli/config/nodeTransform.cjs'); + require('../src/cli'); +} diff --git a/packages/cli-module-test-jest/package.json b/packages/cli-module-test-jest/package.json index 7df933dee9..5fef26bbab 100644 --- a/packages/cli-module-test-jest/package.json +++ b/packages/cli-module-test-jest/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "bin" ], "scripts": { "build": "backstage-cli package build", @@ -35,5 +36,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^" - } + }, + "bin": "bin/cli-module-test-jest" } diff --git a/packages/cli-module-test-jest/src/cli.ts b/packages/cli-module-test-jest/src/cli.ts new file mode 100644 index 0000000000..6d4b925cf1 --- /dev/null +++ b/packages/cli-module-test-jest/src/cli.ts @@ -0,0 +1,26 @@ +/* + * 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 { runCliModule } from '@backstage/cli-node'; +import cliModule from './index'; + +const pkg = require('../package.json') as { name: string; version: string }; + +runCliModule({ + module: cliModule, + name: pkg.name, + version: pkg.version, +}); diff --git a/packages/cli-module-translations/bin/cli-module-translations b/packages/cli-module-translations/bin/cli-module-translations new file mode 100755 index 0000000000..533974f6a9 --- /dev/null +++ b/packages/cli-module-translations/bin/cli-module-translations @@ -0,0 +1,33 @@ +#!/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) { + const { runCliModule } = require('@backstage/cli-node'); + const cliModule = require('..').default; + const pkg = require('../package.json'); + runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); +} else { + require('@backstage/cli/config/nodeTransform.cjs'); + require('../src/cli'); +} diff --git a/packages/cli-module-translations/package.json b/packages/cli-module-translations/package.json index 8818ff32d4..f9e642afe7 100644 --- a/packages/cli-module-translations/package.json +++ b/packages/cli-module-translations/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "bin" ], "scripts": { "build": "backstage-cli package build", @@ -35,5 +36,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^" - } + }, + "bin": "bin/cli-module-translations" } diff --git a/packages/cli-module-translations/src/cli.ts b/packages/cli-module-translations/src/cli.ts new file mode 100644 index 0000000000..6d4b925cf1 --- /dev/null +++ b/packages/cli-module-translations/src/cli.ts @@ -0,0 +1,26 @@ +/* + * 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 { runCliModule } from '@backstage/cli-node'; +import cliModule from './index'; + +const pkg = require('../package.json') as { name: string; version: string }; + +runCliModule({ + module: cliModule, + name: pkg.name, + version: pkg.version, +}); diff --git a/yarn.lock b/yarn.lock index d753c5d9cb..d0f471e49a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2819,6 +2819,8 @@ __metadata: proper-lockfile: "npm:^4.1.2" yaml: "npm:^2.0.0" zod: "npm:^3.25.76" + bin: + cli-module-auth: bin/cli-module-auth languageName: unknown linkType: soft @@ -2881,6 +2883,8 @@ __metadata: webpack: "npm:^5.105.4" webpack-dev-server: "npm:^5.2.3" yn: "npm:^4.0.0" + bin: + cli-module-build: bin/cli-module-build languageName: unknown linkType: soft @@ -2901,6 +2905,8 @@ __metadata: 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/cli-module-config languageName: unknown linkType: soft @@ -2921,6 +2927,8 @@ __metadata: inquirer: "npm:^8.2.0" react-dev-utils: "npm:^12.0.0-next.60" yaml: "npm:^2.0.0" + bin: + cli-module-create-github-app: bin/cli-module-create-github-app languageName: unknown linkType: soft @@ -2935,6 +2943,8 @@ __metadata: cleye: "npm:^2.3.0" fs-extra: "npm:^11.2.0" minimatch: "npm:^10.2.1" + bin: + cli-module-info: bin/cli-module-info languageName: unknown linkType: soft @@ -2953,6 +2963,8 @@ __metadata: fs-extra: "npm:^11.2.0" globby: "npm:^11.0.0" shell-quote: "npm:^1.8.1" + bin: + cli-module-lint: bin/cli-module-lint languageName: unknown linkType: soft @@ -2968,6 +2980,8 @@ __metadata: cleye: "npm:^2.3.0" eslint: "npm:^8.6.0" fs-extra: "npm:^11.2.0" + bin: + cli-module-maintenance: bin/cli-module-maintenance languageName: unknown linkType: soft @@ -2986,6 +3000,8 @@ __metadata: cleye: "npm:^2.3.0" fs-extra: "npm:^11.2.0" msw: "npm:^1.0.0" + bin: + cli-module-migrate: bin/cli-module-migrate languageName: unknown linkType: soft @@ -2995,6 +3011,8 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/cli-node": "workspace:^" + bin: + cli-module-new: bin/cli-module-new languageName: unknown linkType: soft @@ -3004,6 +3022,8 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/cli-node": "workspace:^" + bin: + cli-module-test-jest: bin/cli-module-test-jest languageName: unknown linkType: soft @@ -3013,6 +3033,8 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/cli-node": "workspace:^" + bin: + cli-module-translations: bin/cli-module-translations languageName: unknown linkType: soft From d806b0cc9fdd7989eceb223fd1f4710f5702b099 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 14 Mar 2026 12:34:24 +0100 Subject: [PATCH 114/188] Add automatic discovery of CLI modules from project dependencies The CLI now scans the project root's dependencies and devDependencies for packages with the cli-module role, loading them automatically. Falls back to the built-in set with a deprecation warning when no modules are found. Updated create-app templates and the monorepo root to include all CLI modules as explicit devDependencies. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/cli-discover-modules.md | 31 +++++++++ .changeset/create-app-cli-modules.md | 5 ++ package.json | 11 ++++ packages/cli/src/index.ts | 46 +++++++++---- packages/cli/src/wiring/discoverCliModules.ts | 65 +++++++++++++++++++ .../templates/default-app/package.json.hbs | 11 ++++ .../templates/next-app/package.json.hbs | 11 ++++ yarn.lock | 33 ++++++---- 8 files changed, 191 insertions(+), 22 deletions(-) create mode 100644 .changeset/cli-discover-modules.md create mode 100644 .changeset/create-app-cli-modules.md create mode 100644 packages/cli/src/wiring/discoverCliModules.ts diff --git a/.changeset/cli-discover-modules.md b/.changeset/cli-discover-modules.md new file mode 100644 index 0000000000..428a6eb883 --- /dev/null +++ b/.changeset/cli-discover-modules.md @@ -0,0 +1,31 @@ +--- +'@backstage/cli': minor +--- + +The CLI now automatically discovers CLI modules from the project root's `dependencies` and `devDependencies`. Any installed package with the `cli-module` Backstage role will be loaded automatically without needing to be hardcoded in the CLI itself. + +If no CLI modules are found in the project dependencies, the CLI falls back to the built-in set of modules and prints a deprecation warning. This fallback will be removed in a future release. To prepare for this, add the following CLI modules as `devDependencies` in your root `package.json`: + +```json +{ + "devDependencies": { + "@backstage/cli-module-auth": "backstage:^", + "@backstage/cli-module-build": "backstage:^", + "@backstage/cli-module-config": "backstage:^", + "@backstage/cli-module-create-github-app": "backstage:^", + "@backstage/cli-module-info": "backstage:^", + "@backstage/cli-module-lint": "backstage:^", + "@backstage/cli-module-maintenance": "backstage:^", + "@backstage/cli-module-migrate": "backstage:^", + "@backstage/cli-module-new": "backstage:^", + "@backstage/cli-module-test-jest": "backstage:^", + "@backstage/cli-module-translations": "backstage:^" + } +} +``` + +If you are not using the Backstage Yarn plugin, run the following instead: + +```sh +yarn workspace root add --dev @backstage/cli-module-auth @backstage/cli-module-build @backstage/cli-module-config @backstage/cli-module-create-github-app @backstage/cli-module-info @backstage/cli-module-lint @backstage/cli-module-maintenance @backstage/cli-module-migrate @backstage/cli-module-new @backstage/cli-module-test-jest @backstage/cli-module-translations +``` diff --git a/.changeset/create-app-cli-modules.md b/.changeset/create-app-cli-modules.md new file mode 100644 index 0000000000..4e01b65260 --- /dev/null +++ b/.changeset/create-app-cli-modules.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +The create-app templates now include all standard `@backstage/cli-module-*` packages as `devDependencies`, enabling the CLI's automatic module discovery for newly created projects. diff --git a/package.json b/package.json index d94bdadcbd..962fe132d4 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,17 @@ }, "devDependencies": { "@backstage/cli": "workspace:*", + "@backstage/cli-module-auth": "workspace:*", + "@backstage/cli-module-build": "workspace:*", + "@backstage/cli-module-config": "workspace:*", + "@backstage/cli-module-create-github-app": "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:*", "@backstage/codemods": "workspace:*", "@backstage/create-app": "workspace:*", "@backstage/e2e-test-utils": "workspace:*", diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 8773c3b112..489287c3f2 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -14,20 +14,44 @@ * 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('@backstage/cli-module-build')); - initializer.add(import('@backstage/cli-module-config')); - initializer.add(import('@backstage/cli-module-create-github-app')); - initializer.add(import('@backstage/cli-module-info')); - initializer.add(import('@backstage/cli-module-lint')); - initializer.add(import('@backstage/cli-module-maintenance')); - initializer.add(import('@backstage/cli-module-migrate')); - initializer.add(import('@backstage/cli-module-new')); - initializer.add(import('@backstage/cli-module-test-jest')); - initializer.add(import('@backstage/cli-module-translations')); - initializer.add(import('@backstage/cli-module-auth')); + + const discoveredModules = discoverCliModules(); + + if (discoveredModules.length > 0) { + for (const moduleName of discoveredModules) { + initializer.add(import(moduleName)); + } + } 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 the CLI modules you need as devDependencies ` + + `in your root package.json.\n`, + ), + ); + + initializer.add(import('@backstage/cli-module-build')); + initializer.add(import('@backstage/cli-module-config')); + initializer.add(import('@backstage/cli-module-create-github-app')); + initializer.add(import('@backstage/cli-module-info')); + initializer.add(import('@backstage/cli-module-lint')); + initializer.add(import('@backstage/cli-module-maintenance')); + initializer.add(import('@backstage/cli-module-migrate')); + initializer.add(import('@backstage/cli-module-new')); + initializer.add(import('@backstage/cli-module-test-jest')); + initializer.add(import('@backstage/cli-module-translations')); + initializer.add(import('@backstage/cli-module-auth')); + } + await initializer.run(); })(); diff --git a/packages/cli/src/wiring/discoverCliModules.ts b/packages/cli/src/wiring/discoverCliModules.ts new file mode 100644 index 0000000000..dac80c1344 --- /dev/null +++ b/packages/cli/src/wiring/discoverCliModules.ts @@ -0,0 +1,65 @@ +/* + * 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'; + +/** + * Scans the target project root's package.json for dependencies that are CLI + * modules (packages with `backstage.role === 'cli-module'`). + * + * Returns the names of discovered CLI module packages, or an empty array if + * none are found or the project root cannot be read. + */ +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') { + modules.push(depName); + } + } catch { + // Skip packages that can't be resolved or read + } + } + + return modules; +} diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 229ed5ffa0..ca29f02dc0 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -28,6 +28,17 @@ ], "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", + "@backstage/cli-module-auth": "^{{version '@backstage/cli-module-auth'}}", + "@backstage/cli-module-build": "^{{version '@backstage/cli-module-build'}}", + "@backstage/cli-module-config": "^{{version '@backstage/cli-module-config'}}", + "@backstage/cli-module-create-github-app": "^{{version '@backstage/cli-module-create-github-app'}}", + "@backstage/cli-module-info": "^{{version '@backstage/cli-module-info'}}", + "@backstage/cli-module-lint": "^{{version '@backstage/cli-module-lint'}}", + "@backstage/cli-module-maintenance": "^{{version '@backstage/cli-module-maintenance'}}", + "@backstage/cli-module-migrate": "^{{version '@backstage/cli-module-migrate'}}", + "@backstage/cli-module-new": "^{{version '@backstage/cli-module-new'}}", + "@backstage/cli-module-test-jest": "^{{version '@backstage/cli-module-test-jest'}}", + "@backstage/cli-module-translations": "^{{version '@backstage/cli-module-translations'}}", "@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/package.json.hbs b/packages/create-app/templates/next-app/package.json.hbs index 27d5880c11..110ebc160c 100644 --- a/packages/create-app/templates/next-app/package.json.hbs +++ b/packages/create-app/templates/next-app/package.json.hbs @@ -50,6 +50,17 @@ ], "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", + "@backstage/cli-module-auth": "^{{version '@backstage/cli-module-auth'}}", + "@backstage/cli-module-build": "^{{version '@backstage/cli-module-build'}}", + "@backstage/cli-module-config": "^{{version '@backstage/cli-module-config'}}", + "@backstage/cli-module-create-github-app": "^{{version '@backstage/cli-module-create-github-app'}}", + "@backstage/cli-module-info": "^{{version '@backstage/cli-module-info'}}", + "@backstage/cli-module-lint": "^{{version '@backstage/cli-module-lint'}}", + "@backstage/cli-module-maintenance": "^{{version '@backstage/cli-module-maintenance'}}", + "@backstage/cli-module-migrate": "^{{version '@backstage/cli-module-migrate'}}", + "@backstage/cli-module-new": "^{{version '@backstage/cli-module-new'}}", + "@backstage/cli-module-test-jest": "^{{version '@backstage/cli-module-test-jest'}}", + "@backstage/cli-module-translations": "^{{version '@backstage/cli-module-translations'}}", "@backstage/e2e-test-utils": "^{{version '@backstage/e2e-test-utils'}}", "@jest/environment-jsdom-abstract": "^30.0.0", "@playwright/test": "^1.32.3", diff --git a/yarn.lock b/yarn.lock index d0f471e49a..a5abf933a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2801,7 +2801,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-auth@workspace:^, @backstage/cli-module-auth@workspace:packages/cli-module-auth": +"@backstage/cli-module-auth@workspace:*, @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: @@ -2824,7 +2824,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-build@workspace:^, @backstage/cli-module-build@workspace:packages/cli-module-build": +"@backstage/cli-module-build@workspace:*, @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: @@ -2888,7 +2888,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-config@workspace:^, @backstage/cli-module-config@workspace:packages/cli-module-config": +"@backstage/cli-module-config@workspace:*, @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: @@ -2910,7 +2910,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-create-github-app@workspace:^, @backstage/cli-module-create-github-app@workspace:packages/cli-module-create-github-app": +"@backstage/cli-module-create-github-app@workspace:*, @backstage/cli-module-create-github-app@workspace:^, @backstage/cli-module-create-github-app@workspace:packages/cli-module-create-github-app": version: 0.0.0-use.local resolution: "@backstage/cli-module-create-github-app@workspace:packages/cli-module-create-github-app" dependencies: @@ -2932,7 +2932,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-info@workspace:^, @backstage/cli-module-info@workspace:packages/cli-module-info": +"@backstage/cli-module-info@workspace:*, @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: @@ -2948,7 +2948,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-lint@workspace:^, @backstage/cli-module-lint@workspace:packages/cli-module-lint": +"@backstage/cli-module-lint@workspace:*, @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: @@ -2968,7 +2968,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-maintenance@workspace:^, @backstage/cli-module-maintenance@workspace:packages/cli-module-maintenance": +"@backstage/cli-module-maintenance@workspace:*, @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: @@ -2985,7 +2985,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-migrate@workspace:^, @backstage/cli-module-migrate@workspace:packages/cli-module-migrate": +"@backstage/cli-module-migrate@workspace:*, @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: @@ -3005,7 +3005,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-new@workspace:^, @backstage/cli-module-new@workspace:packages/cli-module-new": +"@backstage/cli-module-new@workspace:*, @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: @@ -3016,7 +3016,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-test-jest@workspace:^, @backstage/cli-module-test-jest@workspace:packages/cli-module-test-jest": +"@backstage/cli-module-test-jest@workspace:*, @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: @@ -3027,7 +3027,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-translations@workspace:^, @backstage/cli-module-translations@workspace:packages/cli-module-translations": +"@backstage/cli-module-translations@workspace:*, @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: @@ -45616,6 +45616,17 @@ __metadata: resolution: "root@workspace:." dependencies: "@backstage/cli": "workspace:*" + "@backstage/cli-module-auth": "workspace:*" + "@backstage/cli-module-build": "workspace:*" + "@backstage/cli-module-config": "workspace:*" + "@backstage/cli-module-create-github-app": "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:*" "@backstage/codemods": "workspace:*" "@backstage/create-app": "workspace:*" "@backstage/e2e-test-utils": "workspace:*" From 329f394d8281e3a62292aa57d2039c4cdbe4781b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 14 Mar 2026 12:46:37 +0100 Subject: [PATCH 115/188] Start CLI module packages at version 0.0.0 with introductory changeset Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/introduce-cli-modules.md | 15 +++++++++++++++ packages/cli-module-auth/package.json | 2 +- packages/cli-module-build/package.json | 2 +- packages/cli-module-config/package.json | 2 +- .../cli-module-create-github-app/package.json | 2 +- packages/cli-module-info/package.json | 2 +- packages/cli-module-lint/package.json | 2 +- packages/cli-module-maintenance/package.json | 2 +- packages/cli-module-migrate/package.json | 2 +- packages/cli-module-new/package.json | 2 +- packages/cli-module-test-jest/package.json | 2 +- packages/cli-module-translations/package.json | 2 +- 12 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 .changeset/introduce-cli-modules.md diff --git a/.changeset/introduce-cli-modules.md b/.changeset/introduce-cli-modules.md new file mode 100644 index 0000000000..a69df9164b --- /dev/null +++ b/.changeset/introduce-cli-modules.md @@ -0,0 +1,15 @@ +--- +'@backstage/cli-module-auth': minor +'@backstage/cli-module-build': minor +'@backstage/cli-module-config': minor +'@backstage/cli-module-create-github-app': minor +'@backstage/cli-module-info': minor +'@backstage/cli-module-lint': minor +'@backstage/cli-module-maintenance': minor +'@backstage/cli-module-migrate': minor +'@backstage/cli-module-new': minor +'@backstage/cli-module-test-jest': minor +'@backstage/cli-module-translations': minor +--- + +Initial release of the CLI module packages. Each module provides a set of commands that can be discovered automatically by `@backstage/cli` or executed standalone. diff --git a/packages/cli-module-auth/package.json b/packages/cli-module-auth/package.json index c3f4b55d0e..89d04cac00 100644 --- a/packages/cli-module-auth/package.json +++ b/packages/cli-module-auth/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-module-auth", - "version": "0.1.0", + "version": "0.0.0", "description": "CLI module for Backstage CLI", "backstage": { "role": "cli-module" diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index dd59b6948f..0f11c37624 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-module-build", - "version": "0.1.0", + "version": "0.0.0", "description": "CLI module for Backstage CLI", "backstage": { "role": "cli-module" diff --git a/packages/cli-module-config/package.json b/packages/cli-module-config/package.json index bcf5059280..b73f3d0f71 100644 --- a/packages/cli-module-config/package.json +++ b/packages/cli-module-config/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-module-config", - "version": "0.1.0", + "version": "0.0.0", "description": "CLI module for Backstage CLI", "backstage": { "role": "cli-module" diff --git a/packages/cli-module-create-github-app/package.json b/packages/cli-module-create-github-app/package.json index 81daa72058..4056cbf28c 100644 --- a/packages/cli-module-create-github-app/package.json +++ b/packages/cli-module-create-github-app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-module-create-github-app", - "version": "0.1.0", + "version": "0.0.0", "description": "CLI module for Backstage CLI", "backstage": { "role": "cli-module" diff --git a/packages/cli-module-info/package.json b/packages/cli-module-info/package.json index 50f79da6f0..aafa7908fe 100644 --- a/packages/cli-module-info/package.json +++ b/packages/cli-module-info/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-module-info", - "version": "0.1.0", + "version": "0.0.0", "description": "CLI module for Backstage CLI", "backstage": { "role": "cli-module" diff --git a/packages/cli-module-lint/package.json b/packages/cli-module-lint/package.json index 50edbae5eb..de42609be9 100644 --- a/packages/cli-module-lint/package.json +++ b/packages/cli-module-lint/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-module-lint", - "version": "0.1.0", + "version": "0.0.0", "description": "CLI module for Backstage CLI", "backstage": { "role": "cli-module" diff --git a/packages/cli-module-maintenance/package.json b/packages/cli-module-maintenance/package.json index 69888e502f..4293beb4ff 100644 --- a/packages/cli-module-maintenance/package.json +++ b/packages/cli-module-maintenance/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-module-maintenance", - "version": "0.1.0", + "version": "0.0.0", "description": "CLI module for Backstage CLI", "backstage": { "role": "cli-module" diff --git a/packages/cli-module-migrate/package.json b/packages/cli-module-migrate/package.json index 49faa42261..e29c15fab0 100644 --- a/packages/cli-module-migrate/package.json +++ b/packages/cli-module-migrate/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-module-migrate", - "version": "0.1.0", + "version": "0.0.0", "description": "CLI module for Backstage CLI", "backstage": { "role": "cli-module" diff --git a/packages/cli-module-new/package.json b/packages/cli-module-new/package.json index a49ccaa6d7..7c6d0ea24d 100644 --- a/packages/cli-module-new/package.json +++ b/packages/cli-module-new/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-module-new", - "version": "0.1.0", + "version": "0.0.0", "description": "CLI module for Backstage CLI", "backstage": { "role": "cli-module" diff --git a/packages/cli-module-test-jest/package.json b/packages/cli-module-test-jest/package.json index 5fef26bbab..ea5093999a 100644 --- a/packages/cli-module-test-jest/package.json +++ b/packages/cli-module-test-jest/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-module-test-jest", - "version": "0.1.0", + "version": "0.0.0", "description": "CLI module for Backstage CLI", "backstage": { "role": "cli-module" diff --git a/packages/cli-module-translations/package.json b/packages/cli-module-translations/package.json index f9e642afe7..a104dae0d3 100644 --- a/packages/cli-module-translations/package.json +++ b/packages/cli-module-translations/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-module-translations", - "version": "0.1.0", + "version": "0.0.0", "description": "CLI module for Backstage CLI", "backstage": { "role": "cli-module" From f51e4df2d1d7ee11068d8771f6de4c84bf5189de Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 14 Mar 2026 15:44:54 +0100 Subject: [PATCH 116/188] Fix Prettier formatting and run yarn dedupe Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../cli-module-build/src/lib/bundler/paths.ts | 4 +- .../src/lib/packager/createDistWorkspace.ts | 12 +++--- packages/cli-module-new/src/lib/version.ts | 8 +++- yarn.lock | 38 ++----------------- 4 files changed, 16 insertions(+), 46 deletions(-) diff --git a/packages/cli-module-build/src/lib/bundler/paths.ts b/packages/cli-module-build/src/lib/bundler/paths.ts index 2f2cb0d5d2..d479f74061 100644 --- a/packages/cli-module-build/src/lib/bundler/paths.ts +++ b/packages/cli-module-build/src/lib/bundler/paths.ts @@ -50,9 +50,7 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) { targetHtml = resolvePath(targetDir, `${entry}.html`); if (!fs.pathExistsSync(targetHtml)) { /* eslint-disable-next-line no-restricted-syntax */ - targetHtml = require.resolve( - '@backstage/cli/templates/serve_index.html', - ); + targetHtml = require.resolve('@backstage/cli/templates/serve_index.html'); } } diff --git a/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts index 52ceaaf355..26709788bb 100644 --- a/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts +++ b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts @@ -27,13 +27,11 @@ import partition from 'lodash/partition'; import { run, targetPaths } from '@backstage/cli-common'; -const { - dependencies: cliDependencies, - devDependencies: cliDevDependencies, -} = require('../../../../package.json') as { - dependencies: Record; - devDependencies: Record; -}; +const { dependencies: cliDependencies, devDependencies: cliDevDependencies } = + require('../../../../package.json') as { + dependencies: Record; + devDependencies: Record; + }; import { BuildOptions, buildPackages, diff --git a/packages/cli-module-new/src/lib/version.ts b/packages/cli-module-new/src/lib/version.ts index 57c5646a14..a6f38a38f5 100644 --- a/packages/cli-module-new/src/lib/version.ts +++ b/packages/cli-module-new/src/lib/version.ts @@ -47,9 +47,13 @@ const frontendPluginApi = v('../../../frontend-plugin-api/package.json'); const frontendTestUtils = v('../../../frontend-test-utils/package.json'); const testUtils = v('../../../test-utils/package.json'); const scaffolderNode = v('../../../../plugins/scaffolder-node/package.json'); -const scaffolderNodeTestUtils = v('../../../../plugins/scaffolder-node-test-utils/package.json'); +const scaffolderNodeTestUtils = v( + '../../../../plugins/scaffolder-node-test-utils/package.json', +); const authBackend = v('../../../../plugins/auth-backend/package.json'); -const authBackendModuleGuestProvider = v('../../../../plugins/auth-backend-module-guest-provider/package.json'); +const authBackendModuleGuestProvider = v( + '../../../../plugins/auth-backend-module-guest-provider/package.json', +); const catalogNode = v('../../../../plugins/catalog-node/package.json'); const theme = v('../../../theme/package.json'); const types = v('../../../types/package.json'); diff --git a/yarn.lock b/yarn.lock index a5abf933a3..f67e5a8dae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24933,14 +24933,7 @@ __metadata: languageName: node linkType: hard -"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 - languageName: node - linkType: hard - -"ansi-styles@npm:^6.2.3": +"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 @@ -32595,14 +32588,7 @@ __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 - languageName: node - linkType: hard - -"get-east-asian-width@npm:^1.3.1, get-east-asian-width@npm:^1.5.0": +"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 @@ -32989,20 +32975,13 @@ __metadata: languageName: node linkType: hard -"globals@npm:^17.0.0": +"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 -"globals@npm:^17.3.0": - version: 17.3.0 - resolution: "globals@npm:17.3.0" - checksum: 10/44ba2b7db93eb6a2531dfba09219845e21f2e724a4f400eb59518b180b7d5bcf7f65580530e3d3023d7dc2bdbacf5d265fd87c393f567deb9a2b0472b51c9d5e - languageName: node - linkType: hard - "globalthis@npm:^1.0.1, globalthis@npm:^1.0.3, globalthis@npm:^1.0.4": version: 1.0.4 resolution: "globalthis@npm:1.0.4" @@ -34945,16 +34924,7 @@ __metadata: 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" - dependencies: - get-east-asian-width: "npm:^1.0.0" - checksum: 10/8dfb2d2831b9e87983c136f5c335cd9d14c1402973e357a8ff057904612ed84b8cba196319fabedf9aefe4639e14fe3afe9d9966d1d006ebeb40fe1fed4babe5 - languageName: node - linkType: hard - -"is-fullwidth-code-point@npm:^5.1.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: From 7c8bb02ffebcdf713d4297a831b1be64cbd71f9b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 14 Mar 2026 15:59:28 +0100 Subject: [PATCH 117/188] Fix eslint-webpack-plugin version and regenerate lockfile Downgrade eslint-webpack-plugin in cli-module-build from ^5.0.3 to ^4.2.0 to match the main CLI package, preventing @types/eslint from being resolved to v9 which breaks the eslint-plugin TypeScript checks. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-build/package.json | 2 +- yarn.lock | 297 ++++++++++++------------- 2 files changed, 140 insertions(+), 159 deletions(-) diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index 0f11c37624..453ff90a86 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -54,7 +54,7 @@ "ctrlc-windows": "^2.1.0", "esbuild-loader": "^4.4.2", "eslint-rspack-plugin": "^4.2.1", - "eslint-webpack-plugin": "^5.0.3", + "eslint-webpack-plugin": "^4.2.0", "fork-ts-checker-webpack-plugin": "^9.1.0", "fs-extra": "^11.2.0", "glob": "^7.1.7", diff --git a/yarn.lock b/yarn.lock index f67e5a8dae..2b300bf732 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2858,7 +2858,7 @@ __metadata: ctrlc-windows: "npm:^2.1.0" esbuild-loader: "npm:^4.4.2" eslint-rspack-plugin: "npm:^4.2.1" - eslint-webpack-plugin: "npm:^5.0.3" + eslint-webpack-plugin: "npm:^4.2.0" fork-ts-checker-webpack-plugin: "npm:^9.1.0" fs-extra: "npm:^11.2.0" glob: "npm:^7.1.7" @@ -14587,144 +14587,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 @@ -19236,8 +19236,8 @@ __metadata: linkType: hard "@snyk/dep-graph@npm:^2.12.0": - version: 2.16.3 - resolution: "@snyk/dep-graph@npm:2.16.3" + 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" @@ -19258,7 +19258,7 @@ __metadata: packageurl-js: "npm:2.0.1" semver: "npm:^7.0.0" tslib: "npm:^2" - checksum: 10/609c7946d0f64d4df4e68a3b5e2a61e5739f8b5dc57b42036f1651694fa1cc8d1537bb0e4a9a437a9fcd08c9b5db9d313c4c59a7e1267fb42993a9f41e78e2c6 + checksum: 10/e51882a2e566030a135d2fff477afbc7e4f8a14946089598fe56dd1e9b1b65fffe1ef8b3c8e2ff42629fc01d79b6d5dfeca1c505413731866c5b730728aee0a4 languageName: node linkType: hard @@ -21456,17 +21456,7 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:*, @types/eslint@npm:^9.6.1": - version: 9.6.1 - resolution: "@types/eslint@npm:9.6.1" - dependencies: - "@types/estree": "npm:*" - "@types/json-schema": "npm:*" - checksum: 10/719fcd255760168a43d0e306ef87548e1e15bffe361d5f4022b0f266575637acc0ecb85604ac97879ee8ae83c6a6d0613b0ed31d0209ddf22a0fe6d608fc56fe - languageName: node - linkType: hard - -"@types/eslint@npm:^8.56.10": +"@types/eslint@npm:*, @types/eslint@npm:^8.56.10": version: 8.56.12 resolution: "@types/eslint@npm:8.56.12" dependencies: @@ -23123,9 +23113,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" @@ -23142,19 +23132,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" @@ -23164,7 +23154,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 @@ -30711,23 +30701,6 @@ __metadata: languageName: node linkType: hard -"eslint-webpack-plugin@npm:^5.0.3": - version: 5.0.3 - resolution: "eslint-webpack-plugin@npm:5.0.3" - dependencies: - "@types/eslint": "npm:^9.6.1" - flatted: "npm:^3.3.3" - jest-worker: "npm:^29.7.0" - micromatch: "npm:^4.0.8" - normalize-path: "npm:^3.0.0" - schema-utils: "npm:^4.3.2" - peerDependencies: - eslint: ^8.0.0 || ^9.0.0 - webpack: ^5.0.0 - checksum: 10/fbf86752ad9493ea70ef025a044c1437e105be51c430762ceac9ae930f4e67c9027d7b1b848969fb3abd285e32ce0fd04382d6e8326958be6b1068415bdd38ab - languageName: node - linkType: hard - "eslint@npm:^8.33.0, eslint@npm:^8.6.0": version: 8.57.1 resolution: "eslint@npm:8.57.1" @@ -31310,13 +31283,13 @@ __metadata: linkType: hard "express-rate-limit@npm:^8.2.1, express-rate-limit@npm:^8.2.2": - version: 8.3.0 - resolution: "express-rate-limit@npm:8.3.0" + version: 8.3.1 + resolution: "express-rate-limit@npm:8.3.1" dependencies: ip-address: "npm:10.1.0" peerDependencies: express: ">= 4.11" - checksum: 10/e896a66fecc10639e65873186fdfb71f19d6af650220eb7ea5450725215c3eed8dc6ddcfa1e68a9db8c9facc3326fbc281512ad3ccd8f107f42a2466ce12c18c + checksum: 10/dd97bfc48c01a6d4c5433203232b5e7a1e55e21322bde49033e5f8c4339584fe671a94096144a0810f4ea21dcec8aaaf15823109627e609f8ed1bc5912a345cf languageName: node linkType: hard @@ -31985,7 +31958,7 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^3.1.0, flatted@npm:^3.2.7, flatted@npm:^3.3.3, flatted@npm:^3.3.4": +"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 @@ -33754,9 +33727,9 @@ __metadata: linkType: hard "hono@npm:^4.11.4": - version: 4.12.5 - resolution: "hono@npm:4.12.5" - checksum: 10/3d03cf2885f7c75325869a178bc94291a17a656002b930dc91456cb177366f6f344c7c90c09707c1fcb4c6e40b1f33fac98f1622628061628cabadcf8cf6d3fd + version: 4.12.7 + resolution: "hono@npm:4.12.7" + checksum: 10/fed37e612730491ba9456f8f68f1b8727a5298cd839fff9641a0b7a95b1e8567a05abb819d32621b40988f166b01140cf7d573c9218dee2741004f48e09564d5 languageName: node linkType: hard @@ -37071,20 +37044,21 @@ __metadata: linkType: hard "knip@npm:^5.42.0": - version: 5.85.0 - resolution: "knip@npm:5.85.0" + 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" @@ -37092,7 +37066,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10/1b80127b24bad3822c2a128e08c30e33db2c716ccb3c1ee19a082d5f04c7d24f049213e15a6369d9449bf60564a86be16e8f986902ba7251bee17a4530c6c954 + checksum: 10/1b8c94f62d2868b0c62499474f462dc09d57e10e73335c01caacff8e0c0b120f441e0881028db30b6a1c185f98a78e2fa727e8faa150bca40c41848263aee04f languageName: node linkType: hard @@ -37300,18 +37274,18 @@ __metadata: linkType: hard "lint-staged@npm:^16.0.0": - version: 16.4.0 - resolution: "lint-staged@npm:16.4.0" + version: 16.3.3 + resolution: "lint-staged@npm:16.3.3" dependencies: commander: "npm:^14.0.3" listr2: "npm:^9.0.5" - picomatch: "npm:^4.0.3" + micromatch: "npm:^4.0.8" string-argv: "npm:^0.3.2" - tinyexec: "npm:^1.0.4" + tinyexec: "npm:^1.0.2" yaml: "npm:^2.8.2" bin: lint-staged: bin/lint-staged.js - checksum: 10/eb7aa0d43e321bbf282ce0c693a3db762aa9b8e5bf29419a49c8877a247cd3a14cf8d519f914f8c8dcd2aea73ac83c0bb44fadb97a30004219e060a780dda74e + checksum: 10/64b20a0aa8710a89db49b4d4384f44e471de8544133cd9f80b1ebeac148144e43b1553aea2b34bf530fd32b554374ee8128c9332033ba138f1a9685964de1cd3 languageName: node linkType: hard @@ -41081,30 +41055,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 @@ -41146,7 +41120,7 @@ __metadata: optional: true "@oxc-resolver/binding-win32-x64-msvc": optional: true - checksum: 10/2d0da140e34a74347d03a233488ad0284bbb252a6d250b7ca78d5a431d04d72b8275d896b8fb0d03846f221bcbcb2395739187d2645bf84d1d1332b74b7720be + checksum: 10/a6c8fdb2ef4bf9bb84f28e58685457de427d31f74373c0fbd6d1106010cab33027fa3b4336b1b86d0df0a089cd73a6060b730b1b24974d56c59f6fa29c559f9d languageName: node linkType: hard @@ -42760,13 +42734,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 @@ -45931,7 +45905,7 @@ __metadata: languageName: node linkType: hard -"schema-utils@npm:^4.0.0, schema-utils@npm:^4.2.0, schema-utils@npm:^4.3.0, schema-utils@npm:^4.3.2, schema-utils@npm:^4.3.3": +"schema-utils@npm:^4.0.0, schema-utils@npm:^4.2.0, schema-utils@npm:^4.3.0, schema-utils@npm:^4.3.3": version: 4.3.3 resolution: "schema-utils@npm:4.3.3" dependencies: @@ -48041,15 +48015,15 @@ __metadata: linkType: hard "tar@npm:^7.4.3, tar@npm:^7.5.6": - version: 7.5.10 - resolution: "tar@npm:7.5.10" + 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/98ba6421a250b233c36a54f7441647bdfee1ed0b916cd57850259a3602154d996f5b8422f67ef5c8ce77f582ed938054775c2873fc7c901e0c7530ed50febc40 + checksum: 10/fb2e77ee858a73936c68e066f4a602d428d6f812e6da0cc1e14a41f99498e4f7fd3535e355fa15157240a5538aa416026cfa6306bb0d1d1c1abf314b1f878e9a languageName: node linkType: hard @@ -48384,10 +48358,10 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^1.0.4": - version: 1.0.4 - resolution: "tinyexec@npm:1.0.4" - checksum: 10/ccebe4044eef6fa5050929df7862fda70b4fb700f15d94aef8ae6109b9d194dbc3a990125d99944fd25b90fe2115e1927f055b909a604c571a81b647ede5757a +"tinyexec@npm:^1.0.2": + version: 1.0.2 + resolution: "tinyexec@npm:1.0.2" + checksum: 10/cb709ed4240e873d3816e67f851d445f5676e0ae3a52931a60ff571d93d388da09108c8057b62351766133ee05ff3159dd56c3a0fbd39a5933c6639ce8771405 languageName: node linkType: hard @@ -49344,6 +49318,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" @@ -49401,9 +49382,9 @@ __metadata: linkType: hard "undici@npm:^7.2.3, undici@npm:^7.22.0": - version: 7.22.0 - resolution: "undici@npm:7.22.0" - checksum: 10/a7a1813ba4b74c0d46cc8dd160386202c05699ffc487c5d882cf40e6d2435c8d6faff3b8f8675d09bd1ef0386e370675c26b59b9a8c8b3f17b9f82a42236a927 + version: 7.24.1 + resolution: "undici@npm:7.24.1" + checksum: 10/0af4ec2fc2ae50b1d0636895793ea2274a89a7cd445690089a1b957c4ac4a0336e48ab9ac48a4e8f5023d4b5eab56368c3f17c75d6b4c81b4281b674d1e60c9f languageName: node linkType: hard @@ -51179,12 +51160,12 @@ __metadata: 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 From 72457cfe98f6ce0a5a8e4f7d8ad7a7f7996ed914 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 14 Mar 2026 16:06:33 +0100 Subject: [PATCH 118/188] Fix incorrect relative package.json paths in cli-module-build Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-build/src/lib/bundler/config.ts | 2 +- .../cli-module-build/src/lib/packager/createDistWorkspace.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli-module-build/src/lib/bundler/config.ts b/packages/cli-module-build/src/lib/bundler/config.ts index 9fbda57f27..77347d66b1 100644 --- a/packages/cli-module-build/src/lib/bundler/config.ts +++ b/packages/cli-module-build/src/lib/bundler/config.ts @@ -32,7 +32,7 @@ import pickBy from 'lodash/pickBy'; import { runOutput, targetPaths } from '@backstage/cli-common'; import { transforms } from './transforms'; -const { version } = require('../../../../package.json') as { version: string }; +const { version } = require('../../../package.json') as { version: string }; import yn from 'yn'; import { hasReactDomClient } from './hasReactDomClient'; import { createWorkspaceLinkingPlugins } from './linkWorkspaces'; diff --git a/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts index 26709788bb..cd9f7657a3 100644 --- a/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts +++ b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts @@ -28,7 +28,7 @@ import partition from 'lodash/partition'; import { run, targetPaths } from '@backstage/cli-common'; const { dependencies: cliDependencies, devDependencies: cliDevDependencies } = - require('../../../../package.json') as { + require('../../../package.json') as { dependencies: Record; devDependencies: Record; }; From 506da6b8be64bed330c91142b9ecabca9ef57ca6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 14 Mar 2026 16:14:54 +0100 Subject: [PATCH 119/188] Fix rollup version in cli-module-build to prevent API report breakage Downgrade rollup from ^4.59.0 to ^4.27.3 to match the main CLI package and avoid the dedupe upgrading all rollup instances to 4.59.0, which breaks API report generation. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-build/package.json | 2 +- yarn.lock | 216 +++++++++++-------------- 2 files changed, 94 insertions(+), 124 deletions(-) diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index 453ff90a86..374fe982d7 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -87,7 +87,7 @@ "@types/npm-packlist": "^3.0.0", "@types/shell-quote": "^1.7.5", "cross-spawn": "^7.0.6", - "rollup": "^4.59.0", + "rollup": "^4.27.3", "ts-morph": "^24.0.0" }, "bin": "bin/cli-module-build" diff --git a/yarn.lock b/yarn.lock index 2b300bf732..1445578ad3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2871,7 +2871,7 @@ __metadata: postcss: "npm:^8.1.0" postcss-import: "npm:^16.1.0" react-dev-utils: "npm:^12.0.0-next.60" - rollup: "npm:^4.59.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" @@ -17537,177 +17537,156 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.59.0" +"@rollup/rollup-android-arm-eabi@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.53.3" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-android-arm64@npm:4.59.0" +"@rollup/rollup-android-arm64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-android-arm64@npm:4.53.3" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-darwin-arm64@npm:4.59.0" +"@rollup/rollup-darwin-arm64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-darwin-arm64@npm:4.53.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-darwin-x64@npm:4.59.0" +"@rollup/rollup-darwin-x64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-darwin-x64@npm:4.53.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.59.0" +"@rollup/rollup-freebsd-arm64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.53.3" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-freebsd-x64@npm:4.59.0" +"@rollup/rollup-freebsd-x64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-freebsd-x64@npm:4.53.3" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.53.3" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.59.0" +"@rollup/rollup-linux-arm-musleabihf@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.53.3" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.59.0" +"@rollup/rollup-linux-arm64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.53.3" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.59.0" +"@rollup/rollup-linux-arm64-musl@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.53.3" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loong64-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.59.0" +"@rollup/rollup-linux-loong64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.53.3" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-loong64-musl@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-loong64-musl@npm:4.59.0" - conditions: os=linux & cpu=loong64 & libc=musl - languageName: node - linkType: hard - -"@rollup/rollup-linux-ppc64-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.59.0" +"@rollup/rollup-linux-ppc64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.53.3" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-musl@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.59.0" - conditions: os=linux & cpu=ppc64 & libc=musl - languageName: node - linkType: hard - -"@rollup/rollup-linux-riscv64-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.59.0" +"@rollup/rollup-linux-riscv64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.53.3" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.59.0" +"@rollup/rollup-linux-riscv64-musl@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.53.3" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.59.0" +"@rollup/rollup-linux-s390x-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.53.3" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.59.0" +"@rollup/rollup-linux-x64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.53.3" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.59.0" +"@rollup/rollup-linux-x64-musl@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.53.3" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-openbsd-x64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-openbsd-x64@npm:4.59.0" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - -"@rollup/rollup-openharmony-arm64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-openharmony-arm64@npm:4.59.0" +"@rollup/rollup-openharmony-arm64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.53.3" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.59.0" +"@rollup/rollup-win32-arm64-msvc@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.53.3" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.59.0" +"@rollup/rollup-win32-ia32-msvc@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.53.3" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-win32-x64-gnu@npm:4.59.0" +"@rollup/rollup-win32-x64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.53.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.59.0" +"@rollup/rollup-win32-x64-msvc@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.53.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -45465,35 +45444,32 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.27.3, rollup@npm:^4.43.0, rollup@npm:^4.59.0": - version: 4.59.0 - resolution: "rollup@npm:4.59.0" +"rollup@npm:^4.27.3, rollup@npm:^4.43.0": + version: 4.53.3 + resolution: "rollup@npm:4.53.3" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.59.0" - "@rollup/rollup-android-arm64": "npm:4.59.0" - "@rollup/rollup-darwin-arm64": "npm:4.59.0" - "@rollup/rollup-darwin-x64": "npm:4.59.0" - "@rollup/rollup-freebsd-arm64": "npm:4.59.0" - "@rollup/rollup-freebsd-x64": "npm:4.59.0" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.59.0" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.59.0" - "@rollup/rollup-linux-arm64-gnu": "npm:4.59.0" - "@rollup/rollup-linux-arm64-musl": "npm:4.59.0" - "@rollup/rollup-linux-loong64-gnu": "npm:4.59.0" - "@rollup/rollup-linux-loong64-musl": "npm:4.59.0" - "@rollup/rollup-linux-ppc64-gnu": "npm:4.59.0" - "@rollup/rollup-linux-ppc64-musl": "npm:4.59.0" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.59.0" - "@rollup/rollup-linux-riscv64-musl": "npm:4.59.0" - "@rollup/rollup-linux-s390x-gnu": "npm:4.59.0" - "@rollup/rollup-linux-x64-gnu": "npm:4.59.0" - "@rollup/rollup-linux-x64-musl": "npm:4.59.0" - "@rollup/rollup-openbsd-x64": "npm:4.59.0" - "@rollup/rollup-openharmony-arm64": "npm:4.59.0" - "@rollup/rollup-win32-arm64-msvc": "npm:4.59.0" - "@rollup/rollup-win32-ia32-msvc": "npm:4.59.0" - "@rollup/rollup-win32-x64-gnu": "npm:4.59.0" - "@rollup/rollup-win32-x64-msvc": "npm:4.59.0" + "@rollup/rollup-android-arm-eabi": "npm:4.53.3" + "@rollup/rollup-android-arm64": "npm:4.53.3" + "@rollup/rollup-darwin-arm64": "npm:4.53.3" + "@rollup/rollup-darwin-x64": "npm:4.53.3" + "@rollup/rollup-freebsd-arm64": "npm:4.53.3" + "@rollup/rollup-freebsd-x64": "npm:4.53.3" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.53.3" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.53.3" + "@rollup/rollup-linux-arm64-gnu": "npm:4.53.3" + "@rollup/rollup-linux-arm64-musl": "npm:4.53.3" + "@rollup/rollup-linux-loong64-gnu": "npm:4.53.3" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.53.3" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.53.3" + "@rollup/rollup-linux-riscv64-musl": "npm:4.53.3" + "@rollup/rollup-linux-s390x-gnu": "npm:4.53.3" + "@rollup/rollup-linux-x64-gnu": "npm:4.53.3" + "@rollup/rollup-linux-x64-musl": "npm:4.53.3" + "@rollup/rollup-openharmony-arm64": "npm:4.53.3" + "@rollup/rollup-win32-arm64-msvc": "npm:4.53.3" + "@rollup/rollup-win32-ia32-msvc": "npm:4.53.3" + "@rollup/rollup-win32-x64-gnu": "npm:4.53.3" + "@rollup/rollup-win32-x64-msvc": "npm:4.53.3" "@types/estree": "npm:1.0.8" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -45519,12 +45495,8 @@ __metadata: optional: true "@rollup/rollup-linux-loong64-gnu": optional: true - "@rollup/rollup-linux-loong64-musl": - optional: true "@rollup/rollup-linux-ppc64-gnu": optional: true - "@rollup/rollup-linux-ppc64-musl": - optional: true "@rollup/rollup-linux-riscv64-gnu": optional: true "@rollup/rollup-linux-riscv64-musl": @@ -45535,8 +45507,6 @@ __metadata: optional: true "@rollup/rollup-linux-x64-musl": optional: true - "@rollup/rollup-openbsd-x64": - optional: true "@rollup/rollup-openharmony-arm64": optional: true "@rollup/rollup-win32-arm64-msvc": @@ -45551,7 +45521,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/728237932aad7022c0640cd126b9fe5285f2578099f22a0542229a17785320a6553b74582fa5977877541c1faf27de65ed2750bc89dbb55b525405244a46d9f1 + checksum: 10/e2eff82405061fa907f15dfbf742b1f5fb4b214495c00989bcdbe21da5fcb3f6dec3deabacec491300a53c99da409586cfc77bdf29b411fccb9089b72cd3728d languageName: node linkType: hard From 86509de5c88973ecf3a94765d07f878f40a2cef7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 14 Mar 2026 16:44:50 +0100 Subject: [PATCH 120/188] Fix CI failures: dependencies, API reports, and create-app versions - Move runtime dependencies from devDependencies to dependencies in cli-module-build, cli-module-auth, cli-module-migrate, cli-module-new, cli-module-test-jest, and cli-module-translations - Fix relative package.json paths in cli-module-build - Downgrade rollup in cli-module-build to ^4.27.3 to match the CLI - Downgrade eslint-webpack-plugin to ^4.2.0 to prevent @types/eslint v9 - Add CLI module packages to create-app version helper - Add allow-warnings for CLI module packages in API reports - Generate API report files for all CLI module packages Signed-off-by: Patrik Oldsberg Made-with: Cursor --- package.json | 2 +- packages/cli-module-auth/package.json | 4 +- packages/cli-module-auth/report.api.md | 13 ++++++ packages/cli-module-build/package.json | 10 ++--- packages/cli-module-build/report.api.md | 16 +++++++ .../src/lib/bundler/config.ts | 2 + .../src/lib/packager/createDistWorkspace.ts | 1 + packages/cli-module-config/report.api.md | 23 ++++++++++ .../report.api.md | 13 ++++++ packages/cli-module-info/report.api.md | 13 ++++++ packages/cli-module-lint/report.api.md | 13 ++++++ packages/cli-module-maintenance/report.api.md | 13 ++++++ packages/cli-module-migrate/package.json | 11 ++++- packages/cli-module-migrate/report.api.md | 13 ++++++ packages/cli-module-new/package.json | 22 ++++++++- packages/cli-module-new/report.api.md | 13 ++++++ packages/cli-module-test-jest/package.json | 4 ++ packages/cli-module-test-jest/report.api.md | 13 ++++++ packages/cli-module-translations/package.json | 9 +++- .../cli-module-translations/report.api.md | 13 ++++++ packages/create-app/src/lib/versions.ts | 22 +++++++++ yarn.lock | 45 ++++++++++++++++--- 22 files changed, 268 insertions(+), 20 deletions(-) create mode 100644 packages/cli-module-auth/report.api.md create mode 100644 packages/cli-module-build/report.api.md create mode 100644 packages/cli-module-config/report.api.md create mode 100644 packages/cli-module-create-github-app/report.api.md create mode 100644 packages/cli-module-info/report.api.md create mode 100644 packages/cli-module-lint/report.api.md create mode 100644 packages/cli-module-maintenance/report.api.md create mode 100644 packages/cli-module-migrate/report.api.md create mode 100644 packages/cli-module-new/report.api.md create mode 100644 packages/cli-module-test-jest/report.api.md create mode 100644 packages/cli-module-translations/report.api.md diff --git a/package.json b/package.json index 962fe132d4..a302681d2f 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "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", - "build:api-reports:only": "LANG=en_US.UTF-8 NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --sql-reports --allow-warnings 'packages/backend-app-api,packages/core-components,plugins/+(catalog|catalog-import|kubernetes)' -o ae-undocumented,ae-wrong-input-file-type --validate-release-tags", + "build:api-reports:only": "LANG=en_US.UTF-8 NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --sql-reports --allow-warnings 'packages/backend-app-api,packages/cli-module-*,packages/core-components,plugins/+(catalog|catalog-import|kubernetes)' -o ae-undocumented,ae-wrong-input-file-type --validate-release-tags", "build:backend": "yarn workspace example-backend build", "build:knip-reports": "backstage-repo-tools knip-reports", "build:plugins-report": "node ./scripts/build-plugins-report", diff --git a/packages/cli-module-auth/package.json b/packages/cli-module-auth/package.json index 89d04cac00..6e4111bf03 100644 --- a/packages/cli-module-auth/package.json +++ b/packages/cli-module-auth/package.json @@ -35,6 +35,7 @@ "@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", @@ -46,8 +47,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/fs-extra": "^11.0.0", - "@types/proper-lockfile": "^4", - "cross-fetch": "^4.0.0" + "@types/proper-lockfile": "^4" }, "bin": "bin/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-build/package.json b/packages/cli-module-build/package.json index 374fe982d7..b5232ab955 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -37,6 +37,7 @@ "@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.2", @@ -51,6 +52,7 @@ "chalk": "^4.0.0", "chokidar": "^3.5.3", "cleye": "^2.3.0", + "cross-spawn": "^7.0.6", "ctrlc-windows": "^2.1.0", "esbuild-loader": "^4.4.2", "eslint-rspack-plugin": "^4.2.1", @@ -67,6 +69,7 @@ "postcss": "^8.1.0", "postcss-import": "^16.1.0", "react-dev-utils": "^12.0.0-next.60", + "rollup": "^4.27.3", "rollup-plugin-dts": "^6.1.0", "rollup-plugin-esbuild": "^6.1.1", "rollup-plugin-postcss": "^4.0.0", @@ -74,6 +77,7 @@ "shell-quote": "^1.8.1", "tar": "^7.5.6", "ts-checker-rspack-plugin": "^1.1.5", + "ts-morph": "^24.0.0", "webpack": "^5.105.4", "webpack-dev-server": "^5.2.3", "yn": "^4.0.0" @@ -81,14 +85,10 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/module-federation-common": "workspace:^", "@types/fs-extra": "^11.0.0", "@types/lodash": "^4.14.151", "@types/npm-packlist": "^3.0.0", - "@types/shell-quote": "^1.7.5", - "cross-spawn": "^7.0.6", - "rollup": "^4.27.3", - "ts-morph": "^24.0.0" + "@types/shell-quote": "^1.7.5" }, "bin": "bin/cli-module-build" } diff --git a/packages/cli-module-build/report.api.md b/packages/cli-module-build/report.api.md new file mode 100644 index 0000000000..6309f5b839 --- /dev/null +++ b/packages/cli-module-build/report.api.md @@ -0,0 +1,16 @@ +## 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'; + +// Warning: (ae-missing-release-tag) "buildPlugin" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +const buildPlugin: CliModule; +export { buildPlugin }; +export default buildPlugin; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-module-build/src/lib/bundler/config.ts b/packages/cli-module-build/src/lib/bundler/config.ts index 77347d66b1..f2fdf84b25 100644 --- a/packages/cli-module-build/src/lib/bundler/config.ts +++ b/packages/cli-module-build/src/lib/bundler/config.ts @@ -32,7 +32,9 @@ import pickBy from 'lodash/pickBy'; import { runOutput, targetPaths } from '@backstage/cli-common'; import { transforms } from './transforms'; + const { version } = require('../../../package.json') as { version: string }; + import yn from 'yn'; import { hasReactDomClient } from './hasReactDomClient'; import { createWorkspaceLinkingPlugins } from './linkWorkspaces'; diff --git a/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts index cd9f7657a3..5930d3b570 100644 --- a/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts +++ b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts @@ -32,6 +32,7 @@ const { dependencies: cliDependencies, devDependencies: cliDevDependencies } = dependencies: Record; devDependencies: Record; }; + import { BuildOptions, buildPackages, diff --git a/packages/cli-module-config/report.api.md b/packages/cli-module-config/report.api.md new file mode 100644 index 0000000000..185bec4e05 --- /dev/null +++ b/packages/cli-module-config/report.api.md @@ -0,0 +1,23 @@ +## 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'; + +// Warning: (ae-missing-release-tag) "configOption" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const configOption: readonly [ + '--config ', + 'Config files to load instead of app-config.yaml', + (opt: string, opts: string[]) => string[], + string[], +]; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-module-create-github-app/report.api.md b/packages/cli-module-create-github-app/report.api.md new file mode 100644 index 0000000000..16ff642c69 --- /dev/null +++ b/packages/cli-module-create-github-app/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-create-github-app" + +> 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-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-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-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-module-migrate/package.json b/packages/cli-module-migrate/package.json index e29c15fab0..b558cfab19 100644 --- a/packages/cli-module-migrate/package.json +++ b/packages/cli-module-migrate/package.json @@ -34,16 +34,23 @@ "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" + "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/errors": "workspace:^", "@backstage/test-utils": "workspace:^", "@types/fs-extra": "^11.0.0", + "@types/semver": "^7", "msw": "^1.0.0" }, "bin": "bin/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-new/package.json b/packages/cli-module-new/package.json index 7c6d0ea24d..b3a786d113 100644 --- a/packages/cli-module-new/package.json +++ b/packages/cli-module-new/package.json @@ -32,10 +32,28 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/cli-node": "workspace:^" + "@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/cli": "workspace:^" + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/inquirer": "^8", + "@types/lodash": "^4.14.151", + "@types/recursive-readdir": "^2.2.4" }, "bin": "bin/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-module-test-jest/package.json b/packages/cli-module-test-jest/package.json index ea5093999a..73eabf749e 100644 --- a/packages/cli-module-test-jest/package.json +++ b/packages/cli-module-test-jest/package.json @@ -32,10 +32,14 @@ "test": "backstage-cli package test" }, "dependencies": { + "@backstage/cli-common": "workspace:^", "@backstage/cli-node": "workspace:^" }, "devDependencies": { "@backstage/cli": "workspace:^" }, + "peerDependencies": { + "jest-cli": "^29.0.0 || ^30.0.0" + }, "bin": "bin/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-module-translations/package.json b/packages/cli-module-translations/package.json index a104dae0d3..0bb3714889 100644 --- a/packages/cli-module-translations/package.json +++ b/packages/cli-module-translations/package.json @@ -32,10 +32,15 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/cli-node": "workspace:^" + "@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:^" + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0" }, "bin": "bin/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/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index db865a2db5..bd19366e55 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -38,6 +38,17 @@ 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 cliModuleAuth } from '../../../cli-module-auth/package.json'; +import { version as cliModuleBuild } from '../../../cli-module-build/package.json'; +import { version as cliModuleConfig } from '../../../cli-module-config/package.json'; +import { version as cliModuleCreateGithubApp } from '../../../cli-module-create-github-app/package.json'; +import { version as cliModuleInfo } from '../../../cli-module-info/package.json'; +import { version as cliModuleLint } from '../../../cli-module-lint/package.json'; +import { version as cliModuleMaintenance } from '../../../cli-module-maintenance/package.json'; +import { version as cliModuleMigrate } from '../../../cli-module-migrate/package.json'; +import { version as cliModuleNew } from '../../../cli-module-new/package.json'; +import { version as cliModuleTestJest } from '../../../cli-module-test-jest/package.json'; +import { version as cliModuleTranslations } from '../../../cli-module-translations/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 +118,17 @@ export const packageVersions = { '@backstage/catalog-client': catalogClient, '@backstage/catalog-model': catalogModel, '@backstage/cli': cli, + '@backstage/cli-module-auth': cliModuleAuth, + '@backstage/cli-module-build': cliModuleBuild, + '@backstage/cli-module-config': cliModuleConfig, + '@backstage/cli-module-create-github-app': cliModuleCreateGithubApp, + '@backstage/cli-module-info': cliModuleInfo, + '@backstage/cli-module-lint': cliModuleLint, + '@backstage/cli-module-maintenance': cliModuleMaintenance, + '@backstage/cli-module-migrate': cliModuleMigrate, + '@backstage/cli-module-new': cliModuleNew, + '@backstage/cli-module-test-jest': cliModuleTestJest, + '@backstage/cli-module-translations': cliModuleTranslations, '@backstage/config': config, '@backstage/core-app-api': coreAppApi, '@backstage/core-compat-api': coreCompatApi, diff --git a/yarn.lock b/yarn.lock index 1445578ad3..7c572a7c5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2994,12 +2994,19 @@ __metadata: "@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/cli-module-migrate languageName: unknown @@ -3010,7 +3017,25 @@ __metadata: resolution: "@backstage/cli-module-new@workspace:packages/cli-module-new" dependencies: "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" "@backstage/cli-node": "workspace:^" + "@backstage/errors": "workspace:^" + "@types/fs-extra": "npm:^11.0.0" + "@types/inquirer": "npm:^8" + "@types/lodash": "npm:^4.14.151" + "@types/recursive-readdir": "npm:^2.2.4" + 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/cli-module-new languageName: unknown @@ -3021,7 +3046,10 @@ __metadata: resolution: "@backstage/cli-module-test-jest@workspace:packages/cli-module-test-jest" dependencies: "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" "@backstage/cli-node": "workspace:^" + peerDependencies: + jest-cli: ^29.0.0 || ^30.0.0 bin: cli-module-test-jest: bin/cli-module-test-jest languageName: unknown @@ -3032,7 +3060,12 @@ __metadata: 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/cli-module-translations languageName: unknown @@ -21642,7 +21675,7 @@ __metadata: languageName: node linkType: hard -"@types/inquirer@npm:^8.1.3": +"@types/inquirer@npm:^8, @types/inquirer@npm:^8.1.3": version: 8.2.12 resolution: "@types/inquirer@npm:8.2.12" dependencies: @@ -22416,7 +22449,7 @@ __metadata: languageName: node linkType: hard -"@types/recursive-readdir@npm:^2.2.0": +"@types/recursive-readdir@npm:^2.2.0, @types/recursive-readdir@npm:^2.2.4": version: 2.2.4 resolution: "@types/recursive-readdir@npm:2.2.4" dependencies: @@ -22513,10 +22546,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 From 95a5771b47ef66d93bc388ce266ae837c52bbbef Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 14 Mar 2026 16:57:58 +0100 Subject: [PATCH 121/188] Fix remaining undeclared imports in cli-module-new and cli-module-test-jest Add missing devDependencies to cli-module-new and add cleye, yargs, and jest-cli peer dependency to cli-module-test-jest. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-new/package.json | 3 +++ packages/cli-module-test-jest/package.json | 4 +++- packages/cli-module-test-jest/src/commands/package/test.ts | 1 + yarn.lock | 7 ++++++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/cli-module-new/package.json b/packages/cli-module-new/package.json index b3a786d113..b0d22e1701 100644 --- a/packages/cli-module-new/package.json +++ b/packages/cli-module-new/package.json @@ -49,7 +49,10 @@ "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", "@types/lodash": "^4.14.151", diff --git a/packages/cli-module-test-jest/package.json b/packages/cli-module-test-jest/package.json index 73eabf749e..ea927e71ae 100644 --- a/packages/cli-module-test-jest/package.json +++ b/packages/cli-module-test-jest/package.json @@ -33,7 +33,9 @@ }, "dependencies": { "@backstage/cli-common": "workspace:^", - "@backstage/cli-node": "workspace:^" + "@backstage/cli-node": "workspace:^", + "cleye": "^2.3.0", + "yargs": "^17.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/packages/cli-module-test-jest/src/commands/package/test.ts b/packages/cli-module-test-jest/src/commands/package/test.ts index 035db8272f..6c9ad50e5b 100644 --- a/packages/cli-module-test-jest/src/commands/package/test.ts +++ b/packages/cli-module-test-jest/src/commands/package/test.ts @@ -96,5 +96,6 @@ export default async ({ args }: CliCommandContext) => { process.exit(1); } + // eslint-disable-next-line @backstage/no-undeclared-imports await require('jest').run(args); }; diff --git a/yarn.lock b/yarn.lock index 7c572a7c5a..94af36670b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3016,10 +3016,13 @@ __metadata: 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" "@types/lodash": "npm:^4.14.151" @@ -3048,6 +3051,8 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" "@backstage/cli-node": "workspace:^" + cleye: "npm:^2.3.0" + yargs: "npm:^17.0.0" peerDependencies: jest-cli: ^29.0.0 || ^30.0.0 bin: @@ -51109,7 +51114,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:17.7.2, yargs@npm:^17.1.1, yargs@npm:^17.3.1, yargs@npm:^17.7.1, yargs@npm:^17.7.2": +"yargs@npm:17.7.2, yargs@npm:^17.0.0, yargs@npm:^17.1.1, yargs@npm:^17.3.1, yargs@npm:^17.7.1, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: From 2fae0356135a91811fefa6530511eb25c6d9e69e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 14 Mar 2026 17:12:41 +0100 Subject: [PATCH 122/188] Move @types packages to devDependencies in cli-module-config and cli-module-create-github-app Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-config/package.json | 4 ++-- packages/cli-module-create-github-app/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli-module-config/package.json b/packages/cli-module-config/package.json index b73f3d0f71..4c281dcaa3 100644 --- a/packages/cli-module-config/package.json +++ b/packages/cli-module-config/package.json @@ -38,7 +38,6 @@ "@backstage/config-loader": "workspace:^", "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", - "@types/json-schema": "^7.0.6", "chalk": "^4.0.0", "cleye": "^2.3.0", "json-schema": "^0.4.0", @@ -46,7 +45,8 @@ "yaml": "^2.0.0" }, "devDependencies": { - "@backstage/cli": "workspace:^" + "@backstage/cli": "workspace:^", + "@types/json-schema": "^7.0.6" }, "bin": "bin/cli-module-config" } diff --git a/packages/cli-module-create-github-app/package.json b/packages/cli-module-create-github-app/package.json index 4056cbf28c..9b342a4551 100644 --- a/packages/cli-module-create-github-app/package.json +++ b/packages/cli-module-create-github-app/package.json @@ -35,7 +35,6 @@ "@backstage/cli-common": "workspace:^", "@backstage/cli-node": "workspace:^", "@octokit/request": "^8.0.0", - "@types/express": "^4.17.6", "chalk": "^4.0.0", "cleye": "^2.3.0", "express": "^4.22.0", @@ -46,6 +45,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@types/express": "^4.17.6", "@types/fs-extra": "^11.0.0" }, "bin": "bin/cli-module-create-github-app" From f100c26c920fa1158b3942a541d6814fc4c28dc7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 14 Mar 2026 20:25:16 +0100 Subject: [PATCH 123/188] Fix relative paths in cli-module-translations test The test file was moved from packages/cli/src/modules/translations/lib/ to packages/cli-module-translations/src/lib/ but the relative paths to the repo root tsconfig.json and test fixtures were not updated. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/lib/extractTranslations.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cli-module-translations/src/lib/extractTranslations.test.ts b/packages/cli-module-translations/src/lib/extractTranslations.test.ts index 20ff05b69e..13a94f3a0a 100644 --- a/packages/cli-module-translations/src/lib/extractTranslations.test.ts +++ b/packages/cli-module-translations/src/lib/extractTranslations.test.ts @@ -23,11 +23,11 @@ import { describe('extractTranslations', () => { it('extracts translation refs from the org plugin', () => { const project = createTranslationProject( - resolvePath(__dirname, '../../../../../../tsconfig.json'), + resolvePath(__dirname, '../../../../tsconfig.json'), ); const sourceFile = project.addSourceFileAtPath( - resolvePath(__dirname, '../../../../../..', 'plugins/org/src/alpha.tsx'), + resolvePath(__dirname, '../../../..', 'plugins/org/src/alpha.tsx'), ); const refs = extractTranslationRefsFromSourceFile( @@ -59,12 +59,12 @@ describe('extractTranslations', () => { it('ignores non-TranslationRef exports', () => { const project = createTranslationProject( - resolvePath(__dirname, '../../../../../../tsconfig.json'), + 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'), + resolvePath(__dirname, '../../../..', 'plugins/org/src/index.ts'), ); const refs = extractTranslationRefsFromSourceFile( @@ -78,13 +78,13 @@ describe('extractTranslations', () => { it('extracts from the test fixtures translation ref', () => { const project = createTranslationProject( - resolvePath(__dirname, '../../../../../../tsconfig.json'), + resolvePath(__dirname, '../../../../tsconfig.json'), ); const sourceFile = project.addSourceFileAtPath( resolvePath( __dirname, - '../../../../../..', + '../../../..', 'packages/frontend-plugin-api/src/translation/__fixtures__/refs.ts', ), ); From 935c646e897a5b1e976927f807998abc860e8dde Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 01:12:43 +0100 Subject: [PATCH 124/188] Fix CLI module discovery and create-app test mock Resolve CLI module entry points relative to the target project root rather than relying on bare module name resolution. This ensures modules are found even when the CLI package is symlinked from a separate location, such as in the E2E test workspace setup. Also add the missing cli-module-* entries to the create-app tasks test mock that were omitted in a previous commit. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli/src/index.ts | 4 ++-- packages/cli/src/wiring/discoverCliModules.ts | 9 ++++++--- packages/create-app/src/lib/tasks.test.ts | 11 +++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 489287c3f2..f5056f8ba3 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -24,8 +24,8 @@ import { discoverCliModules } from './wiring/discoverCliModules'; const discoveredModules = discoverCliModules(); if (discoveredModules.length > 0) { - for (const moduleName of discoveredModules) { - initializer.add(import(moduleName)); + for (const resolvedPath of discoveredModules) { + initializer.add(import(resolvedPath)); } } else { // No CLI modules found in the project root; fall back to the built-in diff --git a/packages/cli/src/wiring/discoverCliModules.ts b/packages/cli/src/wiring/discoverCliModules.ts index dac80c1344..fb5e8c7469 100644 --- a/packages/cli/src/wiring/discoverCliModules.ts +++ b/packages/cli/src/wiring/discoverCliModules.ts @@ -23,8 +23,10 @@ import { resolve as resolvePath } from 'node:path'; * Scans the target project root's package.json for dependencies that are CLI * modules (packages with `backstage.role === 'cli-module'`). * - * Returns the names of discovered CLI module packages, or an empty array if - * none are found or the project root cannot be read. + * 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; @@ -54,7 +56,8 @@ export function discoverCliModules(): string[] { }); const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf8')); if (PackageRoles.getRoleFromPackage(depPkg) === 'cli-module') { - modules.push(depName); + const resolvedPath = require.resolve(depName, { paths: [rootDir] }); + modules.push(resolvedPath); } } catch { // Skip packages that can't be resolved or read diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index be6852e3ab..0115790b81 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -50,6 +50,17 @@ jest.mock('./versions', () => ({ packageVersions: { root: '1.2.3', '@backstage/cli': '1.0.0', + '@backstage/cli-module-auth': '1.0.0', + '@backstage/cli-module-build': '1.0.0', + '@backstage/cli-module-config': '1.0.0', + '@backstage/cli-module-create-github-app': '1.0.0', + '@backstage/cli-module-info': '1.0.0', + '@backstage/cli-module-lint': '1.0.0', + '@backstage/cli-module-maintenance': '1.0.0', + '@backstage/cli-module-migrate': '1.0.0', + '@backstage/cli-module-new': '1.0.0', + '@backstage/cli-module-test-jest': '1.0.0', + '@backstage/cli-module-translations': '1.0.0', '@backstage/backend-defaults': '1.0.0', '@backstage/backend-tasks': '1.0.0', '@backstage/catalog-model': '1.0.0', From 99af21a711a2c2f34714bb74490260510af2dbbd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 01:26:54 +0100 Subject: [PATCH 125/188] Add stderr logging to E2E plugin creation step Capture and print stdout and stderr when the plugin creation command fails, so that the actual error is visible in CI logs. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/e2e-test/src/commands/runCommand.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/e2e-test/src/commands/runCommand.ts b/packages/e2e-test/src/commands/runCommand.ts index da39351400..b5d951017d 100644 --- a/packages/e2e-test/src/commands/runCommand.ts +++ b/packages/e2e-test/src/commands/runCommand.ts @@ -383,12 +383,22 @@ 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(); + try { + await child.waitForExit(); + } catch (error) { + print(`stdout: ${stdout}`); + print(`stderr: ${stderr}`); + throw error; + } const pluginDir = resolvePath( appDir, From 2c952496bf1432552f314f8172219f6520f00434 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 01:40:26 +0100 Subject: [PATCH 126/188] Use static imports for version extraction in cli-module-new Convert dynamic require() calls to static import statements so that Rollup can inline the version values at build time. The cli-module role uses preserveModules in Rollup, which cannot resolve dynamic requires. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-new/src/lib/version.ts | 51 ++++++++++------------ 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/packages/cli-module-new/src/lib/version.ts b/packages/cli-module-new/src/lib/version.ts index a6f38a38f5..fd65135f4c 100644 --- a/packages/cli-module-new/src/lib/version.ts +++ b/packages/cli-module-new/src/lib/version.ts @@ -30,34 +30,29 @@ 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. */ -type Pkg = { version: string }; -const v = (path: string) => (require(path) as Pkg).version; -const backendPluginApi = v('../../../backend-plugin-api/package.json'); -const backendTestUtils = v('../../../backend-test-utils/package.json'); -const catalogClient = v('../../../catalog-client/package.json'); -const cli = v('../../../cli/package.json'); -const config = v('../../../config/package.json'); -const coreAppApi = v('../../../core-app-api/package.json'); -const coreComponents = v('../../../core-components/package.json'); -const corePluginApi = v('../../../core-plugin-api/package.json'); -const devUtils = v('../../../dev-utils/package.json'); -const errors = v('../../../errors/package.json'); -const frontendDefaults = v('../../../frontend-defaults/package.json'); -const frontendPluginApi = v('../../../frontend-plugin-api/package.json'); -const frontendTestUtils = v('../../../frontend-test-utils/package.json'); -const testUtils = v('../../../test-utils/package.json'); -const scaffolderNode = v('../../../../plugins/scaffolder-node/package.json'); -const scaffolderNodeTestUtils = v( - '../../../../plugins/scaffolder-node-test-utils/package.json', -); -const authBackend = v('../../../../plugins/auth-backend/package.json'); -const authBackendModuleGuestProvider = v( - '../../../../plugins/auth-backend-module-guest-provider/package.json', -); -const catalogNode = v('../../../../plugins/catalog-node/package.json'); -const theme = v('../../../theme/package.json'); -const types = v('../../../types/package.json'); -const backendDefaults = v('../../../backend-defaults/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 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 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, From ff593fa8fa4b07655409610bacc217139cf6c9a7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 02:11:14 +0100 Subject: [PATCH 127/188] Capture stderr in E2E plugin creation step Keep stderr capture from the plugin creation child process to aid debugging if the command fails in the future. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/e2e-test/src/commands/runCommand.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/e2e-test/src/commands/runCommand.ts b/packages/e2e-test/src/commands/runCommand.ts index b5d951017d..a86e7f303d 100644 --- a/packages/e2e-test/src/commands/runCommand.ts +++ b/packages/e2e-test/src/commands/runCommand.ts @@ -392,13 +392,7 @@ async function createPlugin(options: { }); print('Waiting for plugin create script to be done'); - try { - await child.waitForExit(); - } catch (error) { - print(`stdout: ${stdout}`); - print(`stderr: ${stderr}`); - throw error; - } + await child.waitForExit(); const pluginDir = resolvePath( appDir, From 9937a8aa8e81a2b31de0c7b40aaf1351b3082029 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 12:40:25 +0100 Subject: [PATCH 128/188] Clean up CLI module dependencies and revert incorrect require() changes Align dependency versions in CLI modules to match the original CLI package versions. Move dependencies that are only used by modules out of the main CLI package, and add missing dependencies to the modules that actually use them. Revert import-to-require conversions in cli-module-build that were incorrectly introduced during the split. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-auth/package.json | 3 + packages/cli-module-build/package.json | 25 +- .../src/lib/bundler/config.ts | 4 +- .../src/lib/packager/createDistWorkspace.ts | 11 +- packages/cli-module-lint/package.json | 3 +- packages/cli-module-new/package.json | 4 +- packages/cli-module-test-jest/package.json | 2 +- packages/cli/package.json | 138 +------- yarn.lock | 328 +++--------------- 9 files changed, 73 insertions(+), 445 deletions(-) diff --git a/packages/cli-module-auth/package.json b/packages/cli-module-auth/package.json index 6e4111bf03..c3e2d47224 100644 --- a/packages/cli-module-auth/package.json +++ b/packages/cli-module-auth/package.json @@ -49,5 +49,8 @@ "@types/fs-extra": "^11.0.0", "@types/proper-lockfile": "^4" }, + "optionalDependencies": { + "keytar": "^7.9.0" + }, "bin": "bin/cli-module-auth" } diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index b5232ab955..7591007756 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -40,7 +40,7 @@ "@backstage/module-federation-common": "workspace:^", "@manypkg/get-packages": "^1.1.3", "@module-federation/enhanced": "^0.21.6", - "@pmmmwh/react-refresh-webpack-plugin": "^0.6.2", + "@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", @@ -49,37 +49,46 @@ "@rspack/dev-server": "^1.1.4", "@rspack/plugin-react-refresh": "^1.4.3", "bfj": "^9.0.2", + "buffer": "^6.0.3", "chalk": "^4.0.0", - "chokidar": "^3.5.3", + "chokidar": "^3.3.1", "cleye": "^2.3.0", - "cross-spawn": "^7.0.6", + "cross-spawn": "^7.0.3", + "css-loader": "^6.5.1", "ctrlc-windows": "^2.1.0", - "esbuild-loader": "^4.4.2", + "esbuild-loader": "^4.0.0", "eslint-rspack-plugin": "^4.2.1", "eslint-webpack-plugin": "^4.2.0", - "fork-ts-checker-webpack-plugin": "^9.1.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.10.1", + "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", - "webpack": "^5.105.4", - "webpack-dev-server": "^5.2.3", + "util": "^0.12.3", + "webpack": "~5.105.0", + "webpack-dev-server": "^5.0.0", + "yml-loader": "^2.1.0", "yn": "^4.0.0" }, "devDependencies": { diff --git a/packages/cli-module-build/src/lib/bundler/config.ts b/packages/cli-module-build/src/lib/bundler/config.ts index f2fdf84b25..08856cd63e 100644 --- a/packages/cli-module-build/src/lib/bundler/config.ts +++ b/packages/cli-module-build/src/lib/bundler/config.ts @@ -32,9 +32,7 @@ import pickBy from 'lodash/pickBy'; import { runOutput, targetPaths } from '@backstage/cli-common'; import { transforms } from './transforms'; - -const { version } = require('../../../package.json') as { version: string }; - +import { version } from '../../../package.json'; import yn from 'yn'; import { hasReactDomClient } from './hasReactDomClient'; import { createWorkspaceLinkingPlugins } from './linkWorkspaces'; diff --git a/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts index 5930d3b570..535d54f9dc 100644 --- a/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts +++ b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts @@ -26,13 +26,10 @@ import * as tar from 'tar'; import partition from 'lodash/partition'; import { run, targetPaths } from '@backstage/cli-common'; - -const { dependencies: cliDependencies, devDependencies: cliDevDependencies } = - require('../../../package.json') as { - dependencies: Record; - devDependencies: Record; - }; - +import { + dependencies as cliDependencies, + devDependencies as cliDevDependencies, +} from '../../../package.json'; import { BuildOptions, buildPackages, diff --git a/packages/cli-module-lint/package.json b/packages/cli-module-lint/package.json index de42609be9..e6b9615e39 100644 --- a/packages/cli-module-lint/package.json +++ b/packages/cli-module-lint/package.json @@ -37,8 +37,9 @@ "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.0.0", + "globby": "^11.1.0", "shell-quote": "^1.8.1" }, "devDependencies": { diff --git a/packages/cli-module-new/package.json b/packages/cli-module-new/package.json index b0d22e1701..bd056c507c 100644 --- a/packages/cli-module-new/package.json +++ b/packages/cli-module-new/package.json @@ -54,9 +54,9 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/test-utils": "workspace:^", "@types/fs-extra": "^11.0.0", - "@types/inquirer": "^8", + "@types/inquirer": "^8.1.3", "@types/lodash": "^4.14.151", - "@types/recursive-readdir": "^2.2.4" + "@types/recursive-readdir": "^2.2.0" }, "bin": "bin/cli-module-new" } diff --git a/packages/cli-module-test-jest/package.json b/packages/cli-module-test-jest/package.json index ea927e71ae..b02167a934 100644 --- a/packages/cli-module-test-jest/package.json +++ b/packages/cli-module-test-jest/package.json @@ -35,7 +35,7 @@ "@backstage/cli-common": "workspace:^", "@backstage/cli-node": "workspace:^", "cleye": "^2.3.0", - "yargs": "^17.0.0" + "yargs": "^16.2.0" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/packages/cli/package.json b/packages/cli/package.json index 189fd8faaa..059737a6f3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -47,7 +47,6 @@ ] }, "dependencies": { - "@backstage/catalog-model": "workspace:^", "@backstage/cli-common": "workspace:^", "@backstage/cli-module-auth": "workspace:^", "@backstage/cli-module-build": "workspace:^", @@ -61,47 +60,21 @@ "@backstage/cli-module-test-jest": "workspace:^", "@backstage/cli-module-translations": "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", - "bfj": "^9.0.2", - "buffer": "^6.0.3", "chalk": "^4.0.0", - "chokidar": "^3.3.1", - "cleye": "^2.3.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", @@ -109,55 +82,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": "^10.2.1", - "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", - "proper-lockfile": "^4.1.2", - "raw-loader": "^4.0.2", - "react-dev-utils": "^12.0.0-next.60", - "react-refresh": "^0.18.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", - "shell-quote": "^1.8.1", - "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:^", @@ -177,91 +108,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/proper-lockfile": "^4", - "@types/recursive-readdir": "^2.2.0", - "@types/rollup-plugin-peer-deps-external": "^2.2.0", - "@types/rollup-plugin-postcss": "^3.1.4", - "@types/shell-quote": "^1.7.5", - "@types/svgo": "^2.6.2", - "@types/terser-webpack-plugin": "^5.0.4", - "@types/webpack-sources": "^3.2.3", - "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.105.0", - "webpack-dev-server": "^5.0.0" - }, - "optionalDependencies": { - "keytar": "^7.9.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.105.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/yarn.lock b/yarn.lock index 94af36670b..d470627b63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2816,9 +2816,13 @@ __metadata: 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/cli-module-auth languageName: unknown @@ -2838,7 +2842,7 @@ __metadata: "@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.2" + "@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" @@ -2851,37 +2855,46 @@ __metadata: "@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.5.3" + chokidar: "npm:^3.3.1" cleye: "npm:^2.3.0" - cross-spawn: "npm:^7.0.6" + cross-spawn: "npm:^7.0.3" + css-loader: "npm:^6.5.1" ctrlc-windows: "npm:^2.1.0" - esbuild-loader: "npm:^4.4.2" + 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.1.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.10.1" + 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" - webpack: "npm:^5.105.4" - webpack-dev-server: "npm:^5.2.3" + 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/cli-module-build @@ -2960,8 +2973,9 @@ __metadata: 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.0.0" + globby: "npm:^11.1.0" shell-quote: "npm:^1.8.1" bin: cli-module-lint: bin/cli-module-lint @@ -3024,9 +3038,9 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/test-utils": "workspace:^" "@types/fs-extra": "npm:^11.0.0" - "@types/inquirer": "npm:^8" + "@types/inquirer": "npm:^8.1.3" "@types/lodash": "npm:^4.14.151" - "@types/recursive-readdir": "npm:^2.2.4" + "@types/recursive-readdir": "npm:^2.2.0" chalk: "npm:^4.0.0" cleye: "npm:^2.3.0" fs-extra: "npm:^11.2.0" @@ -3052,7 +3066,7 @@ __metadata: "@backstage/cli-common": "workspace:^" "@backstage/cli-node": "workspace:^" cleye: "npm:^2.3.0" - yargs: "npm:^17.0.0" + yargs: "npm:^16.2.0" peerDependencies: jest-cli: ^29.0.0 || ^30.0.0 bin: @@ -3106,7 +3120,6 @@ __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-module-auth": "workspace:^" "@backstage/cli-module-build": "workspace:^" @@ -3121,78 +3134,36 @@ __metadata: "@backstage/cli-module-translations": "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/proper-lockfile": "npm:^4" - "@types/recursive-readdir": "npm:^2.2.0" - "@types/rollup-plugin-peer-deps-external": "npm:^2.2.0" - "@types/rollup-plugin-postcss": "npm:^3.1.4" - "@types/shell-quote": "npm:^1.7.5" - "@types/svgo": "npm:^2.6.2" - "@types/terser-webpack-plugin": "npm:^5.0.4" - "@types/webpack-env": "npm:^1.15.2" - "@types/webpack-sources": "npm:^3.2.3" "@typescript-eslint/eslint-plugin": "npm:^8.17.0" "@typescript-eslint/parser": "npm:^8.16.0" - bfj: "npm:^9.0.2" - buffer: "npm:^6.0.3" chalk: "npm:^4.0.0" - chokidar: "npm:^3.3.1" - cleye: "npm:^2.3.0" commander: "npm:^14.0.3" cross-fetch: "npm:^4.0.0" - cross-spawn: "npm:^7.0.3" - css-loader: "npm:^6.5.1" - ctrlc-windows: "npm:^2.1.0" - del: "npm:^8.0.0" - esbuild: "npm:^0.27.0" - esbuild-loader: "npm:^4.0.0" eslint: "npm:^8.6.0" eslint-config-prettier: "npm:^9.0.0" - eslint-formatter-friendly: "npm:^7.0.0" eslint-plugin-deprecation: "npm:^3.0.0" eslint-plugin-import: "npm:^2.31.0" eslint-plugin-jest: "npm:^28.9.0" @@ -3200,108 +3171,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" - keytar: "npm:^7.9.0" - lodash: "npm:^4.17.21" - mini-css-extract-plugin: "npm:^2.4.2" - minimatch: "npm:^10.2.1" - msw: "npm:^1.0.0" - node-stdlib-browser: "npm:^1.3.1" nodemon: "npm:^3.0.1" - npm-packlist: "npm:^5.0.0" - ora: "npm:^5.3.0" - p-queue: "npm:^6.6.2" pirates: "npm:^4.0.6" postcss: "npm:^8.1.0" - postcss-import: "npm:^16.1.0" - process: "npm:^0.11.10" - proper-lockfile: "npm:^4.1.2" - raw-loader: "npm:^4.0.2" - react-dev-utils: "npm:^12.0.0-next.60" - react-refresh: "npm:^0.18.0" - recursive-readdir: "npm:^2.2.2" - replace-in-file: "npm:^7.1.0" - rollup: "npm:^4.27.3" - rollup-plugin-dts: "npm:^6.1.0" - rollup-plugin-esbuild: "npm:^6.1.1" - rollup-plugin-postcss: "npm:^4.0.0" - rollup-pluginutils: "npm:^2.8.2" - semver: "npm:^7.5.3" - shell-quote: "npm:^1.8.1" - style-loader: "npm:^3.3.1" sucrase: "npm:^3.20.2" - swc-loader: "npm:^0.2.3" - tar: "npm:^7.5.6" - terser-webpack-plugin: "npm:^5.1.3" - ts-checker-rspack-plugin: "npm:^1.1.5" - ts-morph: "npm:^24.0.0" - undici: "npm:^7.2.3" - util: "npm:^0.12.3" - webpack: "npm:~5.105.0" - webpack-dev-server: "npm:^5.0.0" yaml: "npm:^2.0.0" - yargs: "npm:^16.2.0" - yml-loader: "npm:^2.1.0" - yn: "npm:^4.0.0" - zod: "npm:^3.25.76" - 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.105.0 - webpack-dev-server: ^5.0.0 - dependenciesMeta: - keytar: - optional: true peerDependenciesMeta: "@jest/environment-jsdom-abstract": optional: true - "@module-federation/enhanced": - optional: true - "@pmmmwh/react-refresh-webpack-plugin": - optional: true - esbuild-loader: - optional: true - eslint-webpack-plugin: - optional: true - fork-ts-checker-webpack-plugin: - optional: true jest-environment-jsdom: optional: true jsdom: optional: true - mini-css-extract-plugin: - optional: true - terser-webpack-plugin: - optional: true - webpack: - optional: true - webpack-dev-server: - optional: true bin: backstage-cli: bin/backstage-cli languageName: unknown @@ -14946,7 +14837,7 @@ __metadata: languageName: node linkType: hard -"@pmmmwh/react-refresh-webpack-plugin@npm:^0.6.0, @pmmmwh/react-refresh-webpack-plugin@npm:^0.6.2": +"@pmmmwh/react-refresh-webpack-plugin@npm:^0.6.0": version: 0.6.2 resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.6.2" dependencies: @@ -18333,13 +18224,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" @@ -20636,7 +20520,7 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:^0.5.0, @swc/helpers@npm:^0.5.17, @swc/helpers@npm:^0.5.8": +"@swc/helpers@npm:^0.5.0, @swc/helpers@npm:^0.5.8": version: 0.5.19 resolution: "@swc/helpers@npm:0.5.19" dependencies: @@ -21440,13 +21324,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" @@ -21680,7 +21557,7 @@ __metadata: languageName: node linkType: hard -"@types/inquirer@npm:^8, @types/inquirer@npm:^8.1.3": +"@types/inquirer@npm:^8.1.3": version: 8.2.12 resolution: "@types/inquirer@npm:8.2.12" dependencies: @@ -22454,7 +22331,7 @@ __metadata: languageName: node linkType: hard -"@types/recursive-readdir@npm:^2.2.0, @types/recursive-readdir@npm:^2.2.4": +"@types/recursive-readdir@npm:^2.2.0": version: 2.2.4 resolution: "@types/recursive-readdir@npm:2.2.4" dependencies: @@ -22519,24 +22396,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" @@ -22638,13 +22497,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" @@ -22715,15 +22567,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" @@ -22751,15 +22594,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" @@ -22835,24 +22669,13 @@ __metadata: languageName: node linkType: hard -"@types/webpack-env@npm:^1.15.2, @types/webpack-env@npm:^1.15.3": +"@types/webpack-env@npm:^1.15.3": version: 1.18.8 resolution: "@types/webpack-env@npm:1.18.8" checksum: 10/f3932f3d6c2530f644cfc898eda1ab8182d6ae57f555c2f0179d813549b639078671b71e4041831fc306c5ebe61f5cdac794fe4ceae281fce8bf67e23661a488 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" @@ -29040,21 +28863,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" @@ -30243,7 +30051,7 @@ __metadata: languageName: node linkType: hard -"esbuild-loader@npm:^4.0.0, esbuild-loader@npm:^4.4.2": +"esbuild-loader@npm:^4.0.0": version: 4.4.2 resolution: "esbuild-loader@npm:4.4.2" dependencies: @@ -32075,7 +31883,7 @@ __metadata: languageName: node linkType: hard -"fork-ts-checker-webpack-plugin@npm:^9.0.0, fork-ts-checker-webpack-plugin@npm:^9.1.0": +"fork-ts-checker-webpack-plugin@npm:^9.0.0": version: 9.1.0 resolution: "fork-ts-checker-webpack-plugin@npm:9.1.0" dependencies: @@ -33003,20 +32811,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" @@ -34278,7 +34072,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 @@ -35087,13 +34881,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" @@ -35101,13 +34888,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" @@ -39113,7 +38893,7 @@ __metadata: languageName: node linkType: hard -"mini-css-extract-plugin@npm:^2.10.1, mini-css-extract-plugin@npm:^2.4.2": +"mini-css-extract-plugin@npm:^2.4.2": version: 2.10.1 resolution: "mini-css-extract-plugin@npm:2.10.1" dependencies: @@ -41860,13 +41640,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" @@ -42892,13 +42665,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" @@ -45423,7 +45189,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: @@ -46534,13 +46300,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" @@ -48139,7 +47898,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:*, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.17": +"terser-webpack-plugin@npm:^5.3.17": version: 5.3.17 resolution: "terser-webpack-plugin@npm:5.3.17" dependencies: @@ -49405,13 +49164,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" @@ -50489,7 +50241,7 @@ __metadata: languageName: node linkType: hard -"webpack-dev-server@npm:^5.0.0, webpack-dev-server@npm:^5.2.3": +"webpack-dev-server@npm:^5.0.0": version: 5.2.3 resolution: "webpack-dev-server@npm:5.2.3" dependencies: @@ -50558,7 +50310,7 @@ __metadata: languageName: node linkType: hard -"webpack@npm:^5, webpack@npm:^5.105.4, webpack@npm:~5.105.0": +"webpack@npm:^5, webpack@npm:~5.105.0": version: 5.105.4 resolution: "webpack@npm:5.105.4" dependencies: @@ -51114,7 +50866,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:17.7.2, yargs@npm:^17.0.0, yargs@npm:^17.1.1, yargs@npm:^17.3.1, yargs@npm:^17.7.1, yargs@npm:^17.7.2": +"yargs@npm:17.7.2, yargs@npm:^17.1.1, yargs@npm:^17.3.1, yargs@npm:^17.7.1, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: From 47b50ef3c46566e1f5f4f9cfa32be47dc2fa500c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 12:56:32 +0100 Subject: [PATCH 129/188] Add back @types/webpack-env to CLI dependencies The CLI provides tsconfig presets that include webpack-env in the types array, so @types/webpack-env must remain a dependency of the CLI package for consuming apps to compile correctly. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli/package.json | 1 + yarn.lock | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 059737a6f3..2158b2b85e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -68,6 +68,7 @@ "@spotify/eslint-config-typescript": "^15.0.0", "@swc/core": "^1.15.6", "@swc/jest": "^0.2.39", + "@types/webpack-env": "^1.15.2", "@typescript-eslint/eslint-plugin": "^8.17.0", "@typescript-eslint/parser": "^8.16.0", "chalk": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index d470627b63..7b20688f16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3157,6 +3157,7 @@ __metadata: "@types/fs-extra": "npm:^11.0.0" "@types/jest": "npm:^30.0.0" "@types/node": "npm:^22.13.14" + "@types/webpack-env": "npm:^1.15.2" "@typescript-eslint/eslint-plugin": "npm:^8.17.0" "@typescript-eslint/parser": "npm:^8.16.0" chalk: "npm:^4.0.0" @@ -22669,7 +22670,7 @@ __metadata: languageName: node linkType: hard -"@types/webpack-env@npm:^1.15.3": +"@types/webpack-env@npm:^1.15.2, @types/webpack-env@npm:^1.15.3": version: 1.18.8 resolution: "@types/webpack-env@npm:1.18.8" checksum: 10/f3932f3d6c2530f644cfc898eda1ab8182d6ae57f555c2f0179d813549b639078671b71e4041831fc306c5ebe61f5cdac794fe4ceae281fce8bf67e23661a488 From 7db7ca5714ca477f96eed7c5d6d1746dcab76632 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 13:35:12 +0100 Subject: [PATCH 130/188] Convert discovered module paths to file URLs for Windows compatibility On Windows, require.resolve() returns paths like D:\...\index.cjs.js which when passed to import() causes ERR_UNSUPPORTED_ESM_URL_SCHEME because the D: prefix is interpreted as a URL protocol. Use pathToFileURL() to produce proper file:// URLs. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli/src/wiring/discoverCliModules.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/wiring/discoverCliModules.ts b/packages/cli/src/wiring/discoverCliModules.ts index fb5e8c7469..e2945d2631 100644 --- a/packages/cli/src/wiring/discoverCliModules.ts +++ b/packages/cli/src/wiring/discoverCliModules.ts @@ -18,6 +18,7 @@ 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 @@ -57,7 +58,7 @@ export function discoverCliModules(): string[] { const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf8')); if (PackageRoles.getRoleFromPackage(depPkg) === 'cli-module') { const resolvedPath = require.resolve(depName, { paths: [rootDir] }); - modules.push(resolvedPath); + modules.push(pathToFileURL(resolvedPath).href); } } catch { // Skip packages that can't be resolved or read From 7781ae5911c56355b230461edcbd11cc26780948 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 15:01:35 +0100 Subject: [PATCH 131/188] Add @backstage/cli-defaults package Introduces a new `@backstage/cli-defaults` package that re-exports all standard CLI modules as a single array, simplifying dependency management for consumers. The CLI's `CliInitializer` is updated to support array exports alongside single module exports. The create-app template, changesets, and CLI fallback are updated to use `@backstage/cli-defaults` instead of listing 11 individual modules. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/cli-defaults-introduce.md | 5 ++ .changeset/cli-discover-modules.md | 24 +++++++--- .changeset/create-app-cli-modules.md | 2 +- packages/cli-defaults/.eslintrc.js | 1 + packages/cli-defaults/catalog-info.yaml | 10 ++++ packages/cli-defaults/package.json | 48 +++++++++++++++++++ packages/cli-defaults/src/index.ts | 40 ++++++++++++++++ packages/cli/package.json | 12 +---- packages/cli/src/index.ts | 17 ++----- packages/cli/src/wiring/CliInitializer.ts | 28 ++++++++--- packages/create-app/src/lib/tasks.test.ts | 1 + packages/create-app/src/lib/versions.ts | 2 + .../templates/default-app/package.json.hbs | 12 +---- yarn.lock | 31 +++++++----- 14 files changed, 172 insertions(+), 61 deletions(-) create mode 100644 .changeset/cli-defaults-introduce.md create mode 100644 packages/cli-defaults/.eslintrc.js create mode 100644 packages/cli-defaults/catalog-info.yaml create mode 100644 packages/cli-defaults/package.json create mode 100644 packages/cli-defaults/src/index.ts diff --git a/.changeset/cli-defaults-introduce.md b/.changeset/cli-defaults-introduce.md new file mode 100644 index 0000000000..b6bb175e62 --- /dev/null +++ b/.changeset/cli-defaults-introduce.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-defaults': minor +--- + +Introduced `@backstage/cli-defaults`, a convenience package that bundles all standard Backstage CLI modules. Install this single package as a `devDependency` to get the full default set of CLI commands without listing each module individually. diff --git a/.changeset/cli-discover-modules.md b/.changeset/cli-discover-modules.md index 428a6eb883..751facdf1d 100644 --- a/.changeset/cli-discover-modules.md +++ b/.changeset/cli-discover-modules.md @@ -4,7 +4,23 @@ The CLI now automatically discovers CLI modules from the project root's `dependencies` and `devDependencies`. Any installed package with the `cli-module` Backstage role will be loaded automatically without needing to be hardcoded in the CLI itself. -If no CLI modules are found in the project dependencies, the CLI falls back to the built-in set of modules and prints a deprecation warning. This fallback will be removed in a future release. To prepare for this, add the following CLI modules as `devDependencies` in your root `package.json`: +If no CLI modules are found in the project dependencies, the CLI falls back to the built-in set of modules and prints a deprecation warning. This fallback will be removed in a future release. To prepare for this, add `@backstage/cli-defaults` as a `devDependency` in your root `package.json`: + +```json +{ + "devDependencies": { + "@backstage/cli-defaults": "backstage:^" + } +} +``` + +If you are not using the Backstage Yarn plugin, run the following instead: + +```sh +yarn workspace root add --dev @backstage/cli-defaults +``` + +For fine-grained control you can instead install individual CLI modules: ```json { @@ -23,9 +39,3 @@ If no CLI modules are found in the project dependencies, the CLI falls back to t } } ``` - -If you are not using the Backstage Yarn plugin, run the following instead: - -```sh -yarn workspace root add --dev @backstage/cli-module-auth @backstage/cli-module-build @backstage/cli-module-config @backstage/cli-module-create-github-app @backstage/cli-module-info @backstage/cli-module-lint @backstage/cli-module-maintenance @backstage/cli-module-migrate @backstage/cli-module-new @backstage/cli-module-test-jest @backstage/cli-module-translations -``` diff --git a/.changeset/create-app-cli-modules.md b/.changeset/create-app-cli-modules.md index 4e01b65260..c1a8ca9a26 100644 --- a/.changeset/create-app-cli-modules.md +++ b/.changeset/create-app-cli-modules.md @@ -2,4 +2,4 @@ '@backstage/create-app': patch --- -The create-app templates now include all standard `@backstage/cli-module-*` packages as `devDependencies`, enabling the CLI's automatic module discovery for newly created projects. +The create-app templates now include `@backstage/cli-defaults` as a `devDependency`, enabling the CLI's automatic module discovery for newly created projects. diff --git a/packages/cli-defaults/.eslintrc.js b/packages/cli-defaults/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-defaults/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 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..587e54cfe9 --- /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-create-github-app": "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/src/index.ts b/packages/cli-defaults/src/index.ts new file mode 100644 index 0000000000..95c9052b1d --- /dev/null +++ b/packages/cli-defaults/src/index.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 auth from '@backstage/cli-module-auth'; +import build from '@backstage/cli-module-build'; +import config from '@backstage/cli-module-config'; +import createGithubApp from '@backstage/cli-module-create-github-app'; +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'; + +export default [ + auth, + build, + config, + createGithubApp, + info, + lint, + maintenance, + migrate, + newModule, + testJest, + translations, +]; diff --git a/packages/cli/package.json b/packages/cli/package.json index 2158b2b85e..600f150654 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -48,17 +48,7 @@ }, "dependencies": { "@backstage/cli-common": "workspace:^", - "@backstage/cli-module-auth": "workspace:^", - "@backstage/cli-module-build": "workspace:^", - "@backstage/cli-module-config": "workspace:^", - "@backstage/cli-module-create-github-app": "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:^", + "@backstage/cli-defaults": "workspace:^", "@backstage/cli-node": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/eslint-plugin": "workspace:^", diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f5056f8ba3..483717fc64 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -35,22 +35,13 @@ import { discoverCliModules } from './wiring/discoverCliModules'; `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 the CLI modules you need as devDependencies ` + - `in your root package.json.\n`, + `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-module-build')); - initializer.add(import('@backstage/cli-module-config')); - initializer.add(import('@backstage/cli-module-create-github-app')); - initializer.add(import('@backstage/cli-module-info')); - initializer.add(import('@backstage/cli-module-lint')); - initializer.add(import('@backstage/cli-module-maintenance')); - initializer.add(import('@backstage/cli-module-migrate')); - initializer.add(import('@backstage/cli-module-new')); - initializer.add(import('@backstage/cli-module-test-jest')); - initializer.add(import('@backstage/cli-module-translations')); - initializer.add(import('@backstage/cli-module-auth')); + initializer.add(import('@backstage/cli-defaults')); } await initializer.run(); diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index a09adbbc89..4e1067d780 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -39,18 +39,23 @@ function isNodeHidden(node: CommandNode): boolean { return children.every(child => isNodeHidden(child)); } -type UninitializedFeature = CliModule | Promise<{ default: CliModule }>; +type UninitializedFeature = + | CliModule + | CliModule[] + | Promise<{ default: CliModule | CliModule[] }>; 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)), ); + } else if (Array.isArray(feature)) { + this.#uninitiazedFeatures.push(Promise.resolve(feature)); } else { this.#uninitiazedFeatures.push(Promise.resolve(feature)); } @@ -68,9 +73,14 @@ export class CliInitializer { } async #doInit() { - const features = await Promise.all(this.#uninitiazedFeatures); - for (const feature of features) { - await this.#register(feature); + const resolved = await Promise.all(this.#uninitiazedFeatures); + for (const featureOrArray of resolved) { + const features = Array.isArray(featureOrArray) + ? featureOrArray + : [featureOrArray]; + for (const feature of features) { + await this.#register(feature); + } } } @@ -186,8 +196,12 @@ export class CliInitializer { /** @internal */ export function unwrapFeature( - feature: CliModule | { default: CliModule }, -): CliModule { + feature: CliModule | CliModule[] | { default: CliModule | CliModule[] }, +): CliModule | CliModule[] { + if (Array.isArray(feature)) { + return feature; + } + if ('$$type' in feature) { return feature; } diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 0115790b81..ff2f3eb4d6 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/cli-module-auth': '1.0.0', '@backstage/cli-module-build': '1.0.0', '@backstage/cli-module-config': '1.0.0', diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index bd19366e55..e770b88f3f 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 cliModuleAuth } from '../../../cli-module-auth/package.json'; import { version as cliModuleBuild } from '../../../cli-module-build/package.json'; import { version as cliModuleConfig } from '../../../cli-module-config/package.json'; @@ -118,6 +119,7 @@ export const packageVersions = { '@backstage/catalog-client': catalogClient, '@backstage/catalog-model': catalogModel, '@backstage/cli': cli, + '@backstage/cli-defaults': cliDefaults, '@backstage/cli-module-auth': cliModuleAuth, '@backstage/cli-module-build': cliModuleBuild, '@backstage/cli-module-config': cliModuleConfig, diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index ca29f02dc0..4262e0fc82 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -28,17 +28,7 @@ ], "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", - "@backstage/cli-module-auth": "^{{version '@backstage/cli-module-auth'}}", - "@backstage/cli-module-build": "^{{version '@backstage/cli-module-build'}}", - "@backstage/cli-module-config": "^{{version '@backstage/cli-module-config'}}", - "@backstage/cli-module-create-github-app": "^{{version '@backstage/cli-module-create-github-app'}}", - "@backstage/cli-module-info": "^{{version '@backstage/cli-module-info'}}", - "@backstage/cli-module-lint": "^{{version '@backstage/cli-module-lint'}}", - "@backstage/cli-module-maintenance": "^{{version '@backstage/cli-module-maintenance'}}", - "@backstage/cli-module-migrate": "^{{version '@backstage/cli-module-migrate'}}", - "@backstage/cli-module-new": "^{{version '@backstage/cli-module-new'}}", - "@backstage/cli-module-test-jest": "^{{version '@backstage/cli-module-test-jest'}}", - "@backstage/cli-module-translations": "^{{version '@backstage/cli-module-translations'}}", + "@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/yarn.lock b/yarn.lock index 7b20688f16..643fa1d573 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2801,6 +2801,25 @@ __metadata: languageName: unknown linkType: soft +"@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-create-github-app": "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:^, @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" @@ -3121,17 +3140,7 @@ __metadata: "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/cli-common": "workspace:^" - "@backstage/cli-module-auth": "workspace:^" - "@backstage/cli-module-build": "workspace:^" - "@backstage/cli-module-config": "workspace:^" - "@backstage/cli-module-create-github-app": "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:^" + "@backstage/cli-defaults": "workspace:^" "@backstage/cli-node": "workspace:^" "@backstage/config": "workspace:^" "@backstage/core-app-api": "workspace:^" From 4f2d7d555bc34ff8cd173db7344b693a34f79889 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 15:03:35 +0100 Subject: [PATCH 132/188] Add README files to CLI module packages Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-defaults/README.md | 26 +++++++++++++++++++ packages/cli-module-auth/README.md | 19 ++++++++++++++ packages/cli-module-build/README.md | 23 ++++++++++++++++ packages/cli-module-config/README.md | 18 +++++++++++++ .../cli-module-create-github-app/README.md | 14 ++++++++++ packages/cli-module-info/README.md | 14 ++++++++++ packages/cli-module-lint/README.md | 15 +++++++++++ packages/cli-module-maintenance/README.md | 15 +++++++++++ packages/cli-module-migrate/README.md | 20 ++++++++++++++ packages/cli-module-new/README.md | 14 ++++++++++ packages/cli-module-test-jest/README.md | 15 +++++++++++ packages/cli-module-translations/README.md | 15 +++++++++++ 12 files changed, 208 insertions(+) create mode 100644 packages/cli-defaults/README.md create mode 100644 packages/cli-module-auth/README.md create mode 100644 packages/cli-module-build/README.md create mode 100644 packages/cli-module-config/README.md create mode 100644 packages/cli-module-create-github-app/README.md create mode 100644 packages/cli-module-info/README.md create mode 100644 packages/cli-module-lint/README.md create mode 100644 packages/cli-module-maintenance/README.md create mode 100644 packages/cli-module-migrate/README.md create mode 100644 packages/cli-module-new/README.md create mode 100644 packages/cli-module-test-jest/README.md create mode 100644 packages/cli-module-translations/README.md diff --git a/packages/cli-defaults/README.md b/packages/cli-defaults/README.md new file mode 100644 index 0000000000..fa75c322df --- /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-create-github-app`](../cli-module-create-github-app) | 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-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-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-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-create-github-app/README.md b/packages/cli-module-create-github-app/README.md new file mode 100644 index 0000000000..b10272ab02 --- /dev/null +++ b/packages/cli-module-create-github-app/README.md @@ -0,0 +1,14 @@ +# @backstage/cli-module-create-github-app + +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-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-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-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-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-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-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-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) From 7879215cca3e0a2d9a6ac34c43929c467b113a89 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 21:16:00 +0100 Subject: [PATCH 133/188] Add CLI module deduplication and improve conflict reporting Individual CLI modules now silently take precedence over array-sourced modules (e.g. from cli-defaults) when their commands overlap. Conflict errors between non-array modules include both package names for easier debugging. Commands reference their parent module instead of storing a raw package name string. createCliModule now validates that packageJson has a name. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-defaults/report.api.md | 13 ++ packages/cli-defaults/src/index.ts | 5 + .../cli-internal/src/InternalCommandNode.ts | 3 +- .../src/cli-module/createCliModule.ts | 6 + .../cli/src/wiring/CliInitializer.test.ts | 134 ++++++++++++++++++ packages/cli/src/wiring/CliInitializer.ts | 59 +++++--- packages/cli/src/wiring/CommandGraph.ts | 45 +++++- 7 files changed, 244 insertions(+), 21 deletions(-) create mode 100644 packages/cli-defaults/report.api.md 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 index 95c9052b1d..0126d0c074 100644 --- a/packages/cli-defaults/src/index.ts +++ b/packages/cli-defaults/src/index.ts @@ -25,6 +25,11 @@ 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, diff --git a/packages/cli-internal/src/InternalCommandNode.ts b/packages/cli-internal/src/InternalCommandNode.ts index 3d93a5128d..bfa1f90baa 100644 --- a/packages/cli-internal/src/InternalCommandNode.ts +++ b/packages/cli-internal/src/InternalCommandNode.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { CliCommand } from '@backstage/cli-node'; +import { CliCommand, CliModule } from '@backstage/cli-node'; import { OpaqueType } from '@internal/opaque'; /** @internal */ @@ -47,6 +47,7 @@ export const OpaqueCommandLeafNode = OpaqueType.create<{ readonly version: 'v1'; readonly name: string; readonly command: CliCommand; + readonly module?: CliModule; }; }>({ type: '@backstage/CommandLeafNode', diff --git a/packages/cli-node/src/cli-module/createCliModule.ts b/packages/cli-node/src/cli-module/createCliModule.ts index 89ed21876f..7d52e9bf43 100644 --- a/packages/cli-node/src/cli-module/createCliModule.ts +++ b/packages/cli-node/src/cli-module/createCliModule.ts @@ -55,6 +55,12 @@ export function createCliModule(options: { 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 = options .init({ addCommand: command => commands.push(command) }) diff --git a/packages/cli/src/wiring/CliInitializer.test.ts b/packages/cli/src/wiring/CliInitializer.test.ts index b5ac9cfaaa..703ec972fc 100644 --- a/packages/cli/src/wiring/CliInitializer.test.ts +++ b/packages/cli/src/wiring/CliInitializer.test.ts @@ -19,6 +19,24 @@ 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(); @@ -239,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 4e1067d780..e2c46cf99b 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -22,7 +22,6 @@ import { } from '@internal/cli'; import type { CommandNode } from '@internal/cli'; import type { CliModule } from '@backstage/cli-node'; -import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; import { version } from './version'; import chalk from 'chalk'; @@ -44,28 +43,42 @@ type UninitializedFeature = | CliModule[] | Promise<{ default: CliModule | CliModule[] }>; +interface TaggedFeature { + feature: CliModule; + 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)); + 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: CliModule) { if (OpaqueCliModule.isType(feature)) { - const internal = OpaqueCliModule.toInternal(feature); - for (const command of await internal.commands) { - this.commandRegistry.addCommand(command); + 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}`); @@ -73,15 +86,29 @@ export class CliInitializer { } async #doInit() { - const resolved = await Promise.all(this.#uninitiazedFeatures); - for (const featureOrArray of resolved) { - const features = Array.isArray(featureOrArray) - ? featureOrArray - : [featureOrArray]; - for (const feature of features) { - await this.#register(feature); + 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); + } } /** diff --git a/packages/cli/src/wiring/CommandGraph.ts b/packages/cli/src/wiring/CommandGraph.ts index b3db5d68bb..b201a3fba3 100644 --- a/packages/cli/src/wiring/CommandGraph.ts +++ b/packages/cli/src/wiring/CommandGraph.ts @@ -17,8 +17,9 @@ import { CommandNode, OpaqueCommandTreeNode, OpaqueCommandLeafNode, + OpaqueCliModule, } from '@internal/cli'; -import { CliCommand } from './types'; +import { CliCommand, CliModule } from './types'; /** * A sparse graph of commands. @@ -30,7 +31,7 @@ export class CommandGraph { * 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: CliCommand) { + add(command: CliCommand, module?: CliModule) { const path = command.path; let current = this.graph; for (let i = 0; i < path.length - 1; i++) { @@ -50,7 +51,11 @@ export class CommandGraph { current.push(next); } 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 = OpaqueCommandTreeNode.toInternal(next).children; @@ -64,13 +69,18 @@ export class CommandGraph { }); 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( OpaqueCommandLeafNode.createInstance('v1', { name: lastName, command, + module, }), ); } @@ -119,3 +129,30 @@ export class CommandGraph { 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`; +} From 2069f642018dcb41f489eeee04ed712b40bcb57a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 21:51:07 +0100 Subject: [PATCH 134/188] Remove extra exports from CLI module packages CLI modules should only export the module itself as a default export. Remove the named `buildPlugin` export from cli-module-build and the unused `configOption` export from cli-module-config. Also remove the API report warning skip for CLI module packages. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- package.json | 8 ++++---- packages/cli-module-build/report.api.md | 7 ++----- packages/cli-module-build/src/index.ts | 4 +--- packages/cli-module-config/report.api.md | 10 ---------- packages/cli-module-config/src/index.ts | 7 ------- 5 files changed, 7 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index a302681d2f..c443b8aa81 100644 --- a/package.json +++ b/package.json @@ -21,15 +21,15 @@ "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", - "build:api-reports:only": "LANG=en_US.UTF-8 NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --sql-reports --allow-warnings 'packages/backend-app-api,packages/cli-module-*,packages/core-components,plugins/+(catalog|catalog-import|kubernetes)' -o ae-undocumented,ae-wrong-input-file-type --validate-release-tags", + "build:api-reports:only": "LANG=en_US.UTF-8 NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --sql-reports --allow-warnings 'packages/backend-app-api,packages/core-components,plugins/+(catalog|catalog-import|kubernetes)' -o ae-undocumented,ae-wrong-input-file-type --validate-release-tags", "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\"", @@ -52,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", diff --git a/packages/cli-module-build/report.api.md b/packages/cli-module-build/report.api.md index 6309f5b839..34634dd448 100644 --- a/packages/cli-module-build/report.api.md +++ b/packages/cli-module-build/report.api.md @@ -5,12 +5,9 @@ ```ts import { CliModule } from '@backstage/cli-node'; -// Warning: (ae-missing-release-tag) "buildPlugin" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -const buildPlugin: CliModule; -export { buildPlugin }; -export default buildPlugin; +const _default: CliModule; +export default _default; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/cli-module-build/src/index.ts b/packages/cli-module-build/src/index.ts index b2fb597249..887a4b562c 100644 --- a/packages/cli-module-build/src/index.ts +++ b/packages/cli-module-build/src/index.ts @@ -17,7 +17,7 @@ import { createCliModule } from '@backstage/cli-node'; import packageJson from '../package.json'; -export const buildPlugin = createCliModule({ +export default createCliModule({ packageJson, init: async reg => { reg.addCommand({ @@ -85,5 +85,3 @@ export const buildPlugin = createCliModule({ }); }, }); - -export default buildPlugin; diff --git a/packages/cli-module-config/report.api.md b/packages/cli-module-config/report.api.md index 185bec4e05..740aef21fb 100644 --- a/packages/cli-module-config/report.api.md +++ b/packages/cli-module-config/report.api.md @@ -5,16 +5,6 @@ ```ts import { CliModule } from '@backstage/cli-node'; -// Warning: (ae-missing-release-tag) "configOption" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const configOption: readonly [ - '--config ', - 'Config files to load instead of app-config.yaml', - (opt: string, opts: string[]) => string[], - string[], -]; - // @public (undocumented) const _default: CliModule; export default _default; diff --git a/packages/cli-module-config/src/index.ts b/packages/cli-module-config/src/index.ts index af2a7143b7..a041f32d6e 100644 --- a/packages/cli-module-config/src/index.ts +++ b/packages/cli-module-config/src/index.ts @@ -16,13 +16,6 @@ import { createCliModule } from '@backstage/cli-node'; import packageJson from '../package.json'; -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 createCliModule({ packageJson, init: async reg => { From 00adaa9902632d16b61731539a680e084d576108 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 22:01:41 +0100 Subject: [PATCH 135/188] Restore findOwnPaths for serve_index.html in build module Move serve_index.html to cli-module-build/templates and use findOwnPaths instead of require.resolve for safer path resolution. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-build/package.json | 7 ++--- .../cli-module-build/src/lib/bundler/paths.ts | 6 +++-- .../templates/serve_index.html | 27 +++++++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 packages/cli-module-build/templates/serve_index.html diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index 7591007756..7f9fa0b37b 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -19,9 +19,11 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "bin": "bin/cli-module-build", "files": [ "dist", - "bin" + "bin", + "templates" ], "scripts": { "build": "backstage-cli package build", @@ -98,6 +100,5 @@ "@types/lodash": "^4.14.151", "@types/npm-packlist": "^3.0.0", "@types/shell-quote": "^1.7.5" - }, - "bin": "bin/cli-module-build" + } } diff --git a/packages/cli-module-build/src/lib/bundler/paths.ts b/packages/cli-module-build/src/lib/bundler/paths.ts index d479f74061..5a7d3b14dc 100644 --- a/packages/cli-module-build/src/lib/bundler/paths.ts +++ b/packages/cli-module-build/src/lib/bundler/paths.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; -import { targetPaths } from '@backstage/cli-common'; +import { targetPaths, findOwnPaths } from '@backstage/cli-common'; export type BundlingPathsOptions = { // bundle entrypoint, e.g. 'src/index' @@ -50,7 +50,9 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) { targetHtml = resolvePath(targetDir, `${entry}.html`); if (!fs.pathExistsSync(targetHtml)) { /* eslint-disable-next-line no-restricted-syntax */ - targetHtml = require.resolve('@backstage/cli/templates/serve_index.html'); + targetHtml = findOwnPaths(__dirname).resolve( + 'templates/serve_index.html', + ); } } 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 + + + +
+ + + From 55f6eb8c6443c64816290afdc6837c275217b0a5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 22:11:43 +0100 Subject: [PATCH 136/188] Move config files to CLI modules with lazy proxies Move jest config files to cli-module-test-jest/config and node transform + webpack-public-path to cli-module-build/config. Replace originals in @backstage/cli/config with lazy proxies that forward to the appropriate module or throw if it is not installed. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-build/.eslintrc.js | 4 +- .../cli-module-build/config/nodeTransform.cjs | 87 ++++ .../config/nodeTransformHooks.mjs | 294 ++++++++++++ .../config/webpack-public-path.js | 31 ++ packages/cli-module-build/package.json | 3 + .../src/lib/bundler/config.ts | 5 +- .../src/lib/runner/runBackend.ts | 5 +- .../src/tests/transforms/transforms.test.ts | 4 +- packages/cli-module-test-jest/.eslintrc.js | 4 +- .../config/getJestEnvironment.js | 49 ++ .../config/jest-environment-jsdom/index.js | 61 +++ packages/cli-module-test-jest/config/jest.js | 422 ++++++++++++++++++ .../config/jestCacheResultProcessor.cjs | 23 + .../config/jestCachingModuleLoader.js | 35 ++ .../config/jestFileTransform.js | 44 ++ .../config/jestRejectNetworkRequests.js | 70 +++ .../config/jestSucraseTransform.js | 87 ++++ .../config/jestSwcTransform.js | 44 ++ .../config/jestYamlTransform.js | 40 ++ packages/cli-module-test-jest/package.json | 26 +- .../src/commands/package/test.ts | 4 +- .../src/commands/repo/test.ts | 3 +- packages/cli/config/getJestEnvironment.js | 36 +- .../config/jest-environment-jsdom/index.js | 52 +-- packages/cli/config/jest.js | 409 +---------------- .../cli/config/jestCacheResultProcessor.cjs | 16 +- .../cli/config/jestCachingModuleLoader.js | 30 +- packages/cli/config/jestFileTransform.js | 39 +- .../cli/config/jestRejectNetworkRequests.js | 63 +-- packages/cli/config/jestSucraseTransform.js | 80 +--- packages/cli/config/jestSwcTransform.js | 37 +- packages/cli/config/jestYamlTransform.js | 35 +- packages/cli/config/nodeTransform.cjs | 80 +--- packages/cli/config/nodeTransformHooks.mjs | 282 +----------- packages/cli/config/webpack-public-path.js | 24 +- packages/cli/package.json | 2 + yarn.lock | 21 + 37 files changed, 1476 insertions(+), 1075 deletions(-) create mode 100644 packages/cli-module-build/config/nodeTransform.cjs create mode 100644 packages/cli-module-build/config/nodeTransformHooks.mjs create mode 100644 packages/cli-module-build/config/webpack-public-path.js create mode 100644 packages/cli-module-test-jest/config/getJestEnvironment.js create mode 100644 packages/cli-module-test-jest/config/jest-environment-jsdom/index.js create mode 100644 packages/cli-module-test-jest/config/jest.js create mode 100644 packages/cli-module-test-jest/config/jestCacheResultProcessor.cjs create mode 100644 packages/cli-module-test-jest/config/jestCachingModuleLoader.js create mode 100644 packages/cli-module-test-jest/config/jestFileTransform.js create mode 100644 packages/cli-module-test-jest/config/jestRejectNetworkRequests.js create mode 100644 packages/cli-module-test-jest/config/jestSucraseTransform.js create mode 100644 packages/cli-module-test-jest/config/jestSwcTransform.js create mode 100644 packages/cli-module-test-jest/config/jestYamlTransform.js diff --git a/packages/cli-module-build/.eslintrc.js b/packages/cli-module-build/.eslintrc.js index e2a53a6ad2..a070dc55ee 100644 --- a/packages/cli-module-build/.eslintrc.js +++ b/packages/cli-module-build/.eslintrc.js @@ -1 +1,3 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + ignorePatterns: ['config/**', 'templates/**'], +}); diff --git a/packages/cli-module-build/config/nodeTransform.cjs b/packages/cli-module-build/config/nodeTransform.cjs new file mode 100644 index 0000000000..15bcdba9bc --- /dev/null +++ b/packages/cli-module-build/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-module-build/config/nodeTransformHooks.mjs b/packages/cli-module-build/config/nodeTransformHooks.mjs new file mode 100644 index 0000000000..592867e57a --- /dev/null +++ b/packages/cli-module-build/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-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 index 7f9fa0b37b..519b24b701 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -23,6 +23,7 @@ "files": [ "dist", "bin", + "config", "templates" ], "scripts": { @@ -50,6 +51,7 @@ "@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", @@ -70,6 +72,7 @@ "node-stdlib-browser": "^1.3.1", "npm-packlist": "^5.0.0", "p-queue": "^6.6.2", + "pirates": "^4.0.6", "postcss": "^8.1.0", "postcss-import": "^16.1.0", "process": "^0.11.10", diff --git a/packages/cli-module-build/src/lib/bundler/config.ts b/packages/cli-module-build/src/lib/bundler/config.ts index 08856cd63e..cee7ba6819 100644 --- a/packages/cli-module-build/src/lib/bundler/config.ts +++ b/packages/cli-module-build/src/lib/bundler/config.ts @@ -29,7 +29,7 @@ import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack'; import fs from 'fs-extra'; import { optimization as optimizationConfig } from './optimization'; import pickBy from 'lodash/pickBy'; -import { runOutput, targetPaths } from '@backstage/cli-common'; +import { findOwnPaths, runOutput, targetPaths } from '@backstage/cli-common'; import { transforms } from './transforms'; import { version } from '../../../package.json'; @@ -330,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-module-build/src/lib/runner/runBackend.ts b/packages/cli-module-build/src/lib/runner/runBackend.ts index 22fd50b3f4..73c3fe08c7 100644 --- a/packages/cli-module-build/src/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 { targetPaths } from '@backstage/cli-common'; +import { findOwnPaths, targetPaths } from '@backstage/cli-common'; import spawn from 'cross-spawn'; const loaderArgs = [ '--enable-source-maps', '--require', - require.resolve('@backstage/cli/config/nodeTransform.cjs'), + /* eslint-disable-next-line no-restricted-syntax */ + findOwnPaths(__dirname).resolve('config/nodeTransform.cjs'), // TODO: Support modules, although there's currently no way to load them since import() is transpiled tp require() ]; diff --git a/packages/cli-module-build/src/tests/transforms/transforms.test.ts b/packages/cli-module-build/src/tests/transforms/transforms.test.ts index 9e4fca8c63..40f40fac2d 100644 --- a/packages/cli-module-build/src/tests/transforms/transforms.test.ts +++ b/packages/cli-module-build/src/tests/transforms/transforms.test.ts @@ -16,6 +16,7 @@ import { execFileSync } from 'node:child_process'; import { resolve as resolvePath } from 'node:path'; +import { findOwnPaths } from '@backstage/cli-common'; import { Output, buildPackage } from '../../lib/builder'; const exportValues = { @@ -55,7 +56,8 @@ function loadFixture(fixture: string) { 'node', [ '--import', - '@backstage/cli/config/nodeTransform.cjs', + /* eslint-disable-next-line no-restricted-syntax */ + findOwnPaths(__dirname).resolve('config/nodeTransform.cjs'), resolvePath(__dirname, `__fixtures__/${fixture}`), ], { encoding: 'utf8' }, diff --git a/packages/cli-module-test-jest/.eslintrc.js b/packages/cli-module-test-jest/.eslintrc.js index e2a53a6ad2..6f313a7a5e 100644 --- a/packages/cli-module-test-jest/.eslintrc.js +++ b/packages/cli-module-test-jest/.eslintrc.js @@ -1 +1,3 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + ignorePatterns: ['config/**'], +}); 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-module-test-jest/config/jestCacheResultProcessor.cjs b/packages/cli-module-test-jest/config/jestCacheResultProcessor.cjs new file mode 100644 index 0000000000..4af401d8b5 --- /dev/null +++ b/packages/cli-module-test-jest/config/jestCacheResultProcessor.cjs @@ -0,0 +1,23 @@ +/* + * 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. + */ + +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 index b02167a934..f83558327c 100644 --- a/packages/cli-module-test-jest/package.json +++ b/packages/cli-module-test-jest/package.json @@ -21,7 +21,8 @@ "types": "src/index.ts", "files": [ "dist", - "bin" + "bin", + "config" ], "scripts": { "build": "backstage-cli package build", @@ -34,14 +35,35 @@ "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-cli": "^29.0.0 || ^30.0.0" + "@jest/environment-jsdom-abstract": "^30.0.0", + "jest-cli": "^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/cli-module-test-jest" } diff --git a/packages/cli-module-test-jest/src/commands/package/test.ts b/packages/cli-module-test-jest/src/commands/package/test.ts index 6c9ad50e5b..b260601f8e 100644 --- a/packages/cli-module-test-jest/src/commands/package/test.ts +++ b/packages/cli-module-test-jest/src/commands/package/test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -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[]) { @@ -30,7 +30,7 @@ export default async ({ args }: CliCommandContext) => { // Only include our config if caller isn't passing their own config if (!includesAnyOf(args, '-c', '--config')) { /* eslint-disable-next-line no-restricted-syntax */ - args.push('--config', require.resolve('@backstage/cli/config/jest')); + args.push('--config', findOwnPaths(__dirname).resolve('config/jest.js')); } if (!includesAnyOf(args, '--no-passWithNoTests', '--passWithNoTests=false')) { diff --git a/packages/cli-module-test-jest/src/commands/repo/test.ts b/packages/cli-module-test-jest/src/commands/repo/test.ts index e45df2bcfc..806d88965b 100644 --- a/packages/cli-module-test-jest/src/commands/repo/test.ts +++ b/packages/cli-module-test-jest/src/commands/repo/test.ts @@ -25,6 +25,7 @@ import { relative as relativePath } from 'node:path'; import { Lockfile, PackageGraph, SuccessCache } from '@backstage/cli-node'; import { + findOwnPaths, runCheck, runOutput, targetPaths, @@ -181,7 +182,7 @@ export default async ({ args, info }: CliCommandContext) => { // Only include our config if caller isn't passing their own config if (!hasFlags('-c', '--config')) { /* eslint-disable-next-line no-restricted-syntax */ - args.push('--config', require.resolve('@backstage/cli/config/jest')); + args.push('--config', findOwnPaths(__dirname).resolve('config/jest.js')); } if (!hasFlags('--passWithNoTests')) { 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 8a88483d42..e06d0e319e 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -14,409 +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', - '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'), + 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 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/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..83815ebea9 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-module-build/config/nodeTransform.cjs'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-build 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..9fe9b327b5 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-module-build/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 600f150654..4ba287a9ef 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,6 +49,8 @@ "dependencies": { "@backstage/cli-common": "workspace:^", "@backstage/cli-defaults": "workspace:^", + "@backstage/cli-module-build": "workspace:^", + "@backstage/cli-module-test-jest": "workspace:^", "@backstage/cli-node": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/eslint-plugin": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 643fa1d573..d87b8e32d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2869,6 +2869,7 @@ __metadata: "@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" @@ -2893,6 +2894,7 @@ __metadata: node-stdlib-browser: "npm:^1.3.1" npm-packlist: "npm:^5.0.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" @@ -3084,10 +3086,27 @@ __metadata: "@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-cli: ^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/cli-module-test-jest languageName: unknown @@ -3141,6 +3160,8 @@ __metadata: "@backstage/catalog-client": "workspace:^" "@backstage/cli-common": "workspace:^" "@backstage/cli-defaults": "workspace:^" + "@backstage/cli-module-build": "workspace:^" + "@backstage/cli-module-test-jest": "workspace:^" "@backstage/cli-node": "workspace:^" "@backstage/config": "workspace:^" "@backstage/core-app-api": "workspace:^" From 6395e35c6a429e22157d981bd97019e856113255 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 23:12:59 +0100 Subject: [PATCH 137/188] Generate CLI reports for cli-module packages Include cli-module role in CLI report generation alongside the existing cli role. Packages without a bin field are silently skipped. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-auth/cli-report.md | 96 ++++++++++ packages/cli-module-build/cli-report.md | 156 ++++++++++++++++ packages/cli-module-config/cli-report.md | 109 +++++++++++ .../cli-report.md | 26 +++ packages/cli-module-info/cli-report.md | 28 +++ packages/cli-module-lint/cli-report.md | 73 ++++++++ packages/cli-module-maintenance/cli-report.md | 52 ++++++ packages/cli-module-migrate/cli-report.md | 105 +++++++++++ packages/cli-module-new/cli-report.md | 34 ++++ packages/cli-module-test-jest/cli-report.md | 170 ++++++++++++++++++ .../cli-module-translations/cli-report.md | 53 ++++++ .../api-reports/categorizePackageDirs.ts | 5 +- .../cli-reports/runCliExtraction.ts | 2 +- 13 files changed, 906 insertions(+), 3 deletions(-) create mode 100644 packages/cli-module-auth/cli-report.md create mode 100644 packages/cli-module-build/cli-report.md create mode 100644 packages/cli-module-config/cli-report.md create mode 100644 packages/cli-module-create-github-app/cli-report.md create mode 100644 packages/cli-module-info/cli-report.md create mode 100644 packages/cli-module-lint/cli-report.md create mode 100644 packages/cli-module-maintenance/cli-report.md create mode 100644 packages/cli-module-migrate/cli-report.md create mode 100644 packages/cli-module-new/cli-report.md create mode 100644 packages/cli-module-test-jest/cli-report.md create mode 100644 packages/cli-module-translations/cli-report.md diff --git a/packages/cli-module-auth/cli-report.md b/packages/cli-module-auth/cli-report.md new file mode 100644 index 0000000000..1b382040a2 --- /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` + +### `cli-module-auth` + +``` +Usage: @backstage/cli-module-auth [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + auth [command] + help [command] +``` + +### `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 +``` + +### `cli-module-auth auth list` + +``` +Usage: @backstage/cli-module-auth auth list + +Options: + -h, --help +``` + +### `cli-module-auth auth login` + +``` +Usage: @backstage/cli-module-auth auth login + +Options: + --backend-url + --instance + --no-browser + -h, --help +``` + +### `cli-module-auth auth logout` + +``` +Usage: @backstage/cli-module-auth auth logout + +Options: + --instance + -h, --help +``` + +### `cli-module-auth auth print-token` + +``` +Usage: @backstage/cli-module-auth auth print-token + +Options: + --instance + -h, --help +``` + +### `cli-module-auth auth select` + +``` +Usage: @backstage/cli-module-auth auth select + +Options: + --instance + -h, --help +``` + +### `cli-module-auth auth show` + +``` +Usage: @backstage/cli-module-auth auth show + +Options: + --instance + -h, --help +``` diff --git a/packages/cli-module-build/cli-report.md b/packages/cli-module-build/cli-report.md new file mode 100644 index 0000000000..b2240e0523 --- /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` + +### `cli-module-build` + +``` +Usage: @backstage/cli-module-build [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + build-workspace + help [command] + package [command] + repo [command] +``` + +### `cli-module-build build-workspace` + +``` +Usage: @backstage/cli-module-build build-workspace [packages...] + +Options: + --always-pack + -h, --help +``` + +### `cli-module-build package` + +``` +Usage: @backstage/cli-module-build package [options] [command] [command] + +Options: + -h, --help + +Commands: + build + clean + help [command] + postpack + prepack + start +``` + +### `cli-module-build package build` + +``` +Usage: @backstage/cli-module-build package build + +Options: + --config + --minify + --module-federation + --role + --skip-build-dependencies + --stats + -h, --help +``` + +### `cli-module-build package clean` + +``` +Usage: @backstage/cli-module-build package clean + +Options: + -h, --help +``` + +### `cli-module-build package postpack` + +``` +Usage: @backstage/cli-module-build package postpack + +Options: + -h, --help +``` + +### `cli-module-build package prepack` + +``` +Usage: @backstage/cli-module-build package prepack + +Options: + -h, --help +``` + +### `cli-module-build package start` + +``` +Usage: @backstage/cli-module-build package start + +Options: + --check + --config + --entrypoint + --inspect + --inspect-brk + --link + --require + --role + -h, --help +``` + +### `cli-module-build repo` + +``` +Usage: @backstage/cli-module-build repo [options] [command] [command] + +Options: + -h, --help + +Commands: + build + clean + help [command] + start +``` + +### `cli-module-build repo build` + +``` +Usage: @backstage/cli-module-build repo build + +Options: + --all + --minify + --since + -h, --help +``` + +### `cli-module-build repo clean` + +``` +Usage: @backstage/cli-module-build repo clean + +Options: + -h, --help +``` + +### `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-config/cli-report.md b/packages/cli-module-config/cli-report.md new file mode 100644 index 0000000000..6cb8a15d62 --- /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` + +### `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] +``` + +### `cli-module-config config` + +``` +Usage: @backstage/cli-module-config config [options] [command] [command] + +Options: + -h, --help + +Commands: + docs + help [command] + schema +``` + +### `cli-module-config config docs` + +``` +Usage: @backstage/cli-module-config config docs + +Options: + --package + -h, --help +``` + +### `cli-module-config config schema` + +``` +Usage: @backstage/cli-module-config config schema + +Options: + --format + --merge + --package + -h, --help +``` + +### `cli-module-config config:check` + +``` +Usage: @backstage/cli-module-config config:check + +Options: + --config + --deprecated + --frontend + --lax + --package + --strict + -h, --help +``` + +### `cli-module-config config:docs` + +``` +Usage: @backstage/cli-module-config config:docs + +Options: + --package + -h, --help +``` + +### `cli-module-config config:print` + +``` +Usage: @backstage/cli-module-config config:print + +Options: + --config + --format + --frontend + --lax + --package + --with-secrets + -h, --help +``` + +### `cli-module-config config:schema` + +``` +Usage: @backstage/cli-module-config config:schema + +Options: + --format + --merge + --package + -h, --help +``` diff --git a/packages/cli-module-create-github-app/cli-report.md b/packages/cli-module-create-github-app/cli-report.md new file mode 100644 index 0000000000..a12ea68bf2 --- /dev/null +++ b/packages/cli-module-create-github-app/cli-report.md @@ -0,0 +1,26 @@ +## CLI Report file for "@backstage/cli-module-create-github-app" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `cli-module-create-github-app` + +``` +Usage: @backstage/cli-module-create-github-app [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + create-github-app + help [command] +``` + +### `cli-module-create-github-app create-github-app` + +``` +Usage: @backstage/cli-module-create-github-app create-github-app + +Options: + -h, --help +``` diff --git a/packages/cli-module-info/cli-report.md b/packages/cli-module-info/cli-report.md new file mode 100644 index 0000000000..f292e46a18 --- /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` + +### `cli-module-info` + +``` +Usage: @backstage/cli-module-info [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + info +``` + +### `cli-module-info info` + +``` +Usage: @backstage/cli-module-info info + +Options: + --format + --include + -h, --help +``` diff --git a/packages/cli-module-lint/cli-report.md b/packages/cli-module-lint/cli-report.md new file mode 100644 index 0000000000..c577759e0c --- /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` + +### `cli-module-lint` + +``` +Usage: @backstage/cli-module-lint [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + package [command] + repo [command] +``` + +### `cli-module-lint package` + +``` +Usage: @backstage/cli-module-lint package [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + lint +``` + +### `cli-module-lint package lint` + +``` +Usage: @backstage/cli-module-lint package lint [directories...] + +Options: + --fix + --format + --max-warnings + --output-file + -h, --help +``` + +### `cli-module-lint repo` + +``` +Usage: @backstage/cli-module-lint repo [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + lint +``` + +### `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-maintenance/cli-report.md b/packages/cli-module-maintenance/cli-report.md new file mode 100644 index 0000000000..66b9950328 --- /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` + +### `cli-module-maintenance` + +``` +Usage: @backstage/cli-module-maintenance [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + repo [command] +``` + +### `cli-module-maintenance repo` + +``` +Usage: @backstage/cli-module-maintenance repo [options] [command] [command] + +Options: + -h, --help + +Commands: + fix + help [command] + list-deprecations +``` + +### `cli-module-maintenance repo fix` + +``` +Usage: @backstage/cli-module-maintenance repo fix + +Options: + --check + --publish + -h, --help +``` + +### `cli-module-maintenance repo list-deprecations` + +``` +Usage: @backstage/cli-module-maintenance repo list-deprecations + +Options: + --json + -h, --help +``` diff --git a/packages/cli-module-migrate/cli-report.md b/packages/cli-module-migrate/cli-report.md new file mode 100644 index 0000000000..06a60cdfaf --- /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` + +### `cli-module-migrate` + +``` +Usage: @backstage/cli-module-migrate [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + migrate [command] + versions:bump + versions:migrate +``` + +### `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 +``` + +### `cli-module-migrate migrate package-exports` + +``` +Usage: @backstage/cli-module-migrate migrate package-exports + +Options: + -h, --help +``` + +### `cli-module-migrate migrate package-lint-configs` + +``` +Usage: @backstage/cli-module-migrate migrate package-lint-configs + +Options: + -h, --help +``` + +### `cli-module-migrate migrate package-roles` + +``` +Usage: @backstage/cli-module-migrate migrate package-roles + +Options: + -h, --help +``` + +### `cli-module-migrate migrate package-scripts` + +``` +Usage: @backstage/cli-module-migrate migrate package-scripts + +Options: + -h, --help +``` + +### `cli-module-migrate migrate react-router-deps` + +``` +Usage: @backstage/cli-module-migrate migrate react-router-deps + +Options: + -h, --help +``` + +### `cli-module-migrate versions:bump` + +``` +Usage: @backstage/cli-module-migrate versions:bump + +Options: + --pattern + --release + --skip-install + --skip-migrate + -h, --help +``` + +### `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-new/cli-report.md b/packages/cli-module-new/cli-report.md new file mode 100644 index 0000000000..2c414fc747 --- /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` + +### `cli-module-new` + +``` +Usage: @backstage/cli-module-new [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + new +``` + +### `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-test-jest/cli-report.md b/packages/cli-module-test-jest/cli-report.md new file mode 100644 index 0000000000..ca1d390ed7 --- /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` + +### `cli-module-test-jest` + +``` +Usage: @backstage/cli-module-test-jest [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + package [command] + repo [command] +``` + +### `cli-module-test-jest package` + +``` +Usage: @backstage/cli-module-test-jest package [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + test +``` + +### `cli-module-test-jest package test` + +``` +Usage: cli-module-test-jest [--config=] [TestPathPatterns] + +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 +``` + +### `cli-module-test-jest repo` + +``` +Usage: @backstage/cli-module-test-jest repo [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + test +``` + +### `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-translations/cli-report.md b/packages/cli-module-translations/cli-report.md new file mode 100644 index 0000000000..9b2d5ef3b4 --- /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` + +### `cli-module-translations` + +``` +Usage: @backstage/cli-module-translations [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + translations [command] +``` + +### `cli-module-translations translations` + +``` +Usage: @backstage/cli-module-translations translations [options] [command] [command] + +Options: + -h, --help + +Commands: + export + help [command] + import +``` + +### `cli-module-translations translations export` + +``` +Usage: @backstage/cli-module-translations translations export + +Options: + --output + --pattern + -h, --help +``` + +### `cli-module-translations translations import` + +``` +Usage: @backstage/cli-module-translations translations import + +Options: + --input + --output + -h, --help +``` diff --git a/packages/repo-tools/src/commands/api-reports/categorizePackageDirs.ts b/packages/repo-tools/src/commands/api-reports/categorizePackageDirs.ts index 973c7b8acb..6fdb890100 100644 --- a/packages/repo-tools/src/commands/api-reports/categorizePackageDirs.ts +++ b/packages/repo-tools/src/commands/api-reports/categorizePackageDirs.ts @@ -54,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 ae49c95f39..7dfbba51ec 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 @@ -132,7 +132,7 @@ export async function runCliExtraction({ const pkgJson = await fs.readJson(resolvePath(fullDir, 'package.json')); if (!pkgJson.bin) { - throw new Error(`CLI Package in ${packageDir} has no bin field`); + continue; } const models = new Array(); From ac96393a12af221ee6fef72fd269fd643ef6fc35 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 23:30:19 +0100 Subject: [PATCH 138/188] Fix jest peer dependency in cli-module-test-jest Change peer dependency from jest-cli to jest to match the actual runtime usage of require('jest') and the original @backstage/cli peer dependency declaration. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-test-jest/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli-module-test-jest/package.json b/packages/cli-module-test-jest/package.json index f83558327c..36dd8fafdf 100644 --- a/packages/cli-module-test-jest/package.json +++ b/packages/cli-module-test-jest/package.json @@ -50,7 +50,7 @@ }, "peerDependencies": { "@jest/environment-jsdom-abstract": "^30.0.0", - "jest-cli": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", "jest-environment-jsdom": "*", "jsdom": "^27.1.0" }, diff --git a/yarn.lock b/yarn.lock index d87b8e32d1..275cf38895 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3097,7 +3097,7 @@ __metadata: yargs: "npm:^16.2.0" peerDependencies: "@jest/environment-jsdom-abstract": ^30.0.0 - jest-cli: ^29.0.0 || ^30.0.0 + jest: ^29.0.0 || ^30.0.0 jest-environment-jsdom: "*" jsdom: ^27.1.0 peerDependenciesMeta: From b76bbfb461a0350f15e15677634ad697c1dd6bde Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 23:54:51 +0100 Subject: [PATCH 139/188] Exclude cli-report files from stale API report detection The runApiExtraction function was detecting cli-report.md files as stale API reports because they matched the report filename pattern. These files are managed by runCliExtraction instead and should be excluded from the stale report check. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/commands/api-reports/api-reports/runApiExtraction.ts | 1 + 1 file changed, 1 insertion(+) 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 787a2cb3f4..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 @@ -174,6 +174,7 @@ export async function runApiExtraction({ 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$/), From bc42b608b1930667053a6fa97956df7ce010ff64 Mon Sep 17 00:00:00 2001 From: shivamtiwari3 <33183708+shivamtiwari3@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:40:46 +0530 Subject: [PATCH 140/188] Fix: replace removed --bui-bg-tint tokens in Table component (fixes #33292) Root cause: The --bui-bg-tint-* CSS tokens were removed from the design system in favour of --bui-bg-neutral-* tokens, but Table.module.css was not updated during the migration, leaving row hover, selected, pressed, and disabled states with no visual effect. Fix: Replace --bui-bg-tint-hover/pressed/disabled with the equivalent --bui-bg-neutral-1-hover/pressed/disabled tokens, matching the migration mapping documented in the CHANGELOG. Signed-off-by: shivamtiwari3 <33183708+shivamtiwari3@users.noreply.github.com> --- .changeset/fix-table-bui-bg-tint-tokens.md | 5 +++++ packages/ui/src/components/Table/Table.module.css | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-table-bui-bg-tint-tokens.md diff --git a/.changeset/fix-table-bui-bg-tint-tokens.md b/.changeset/fix-table-bui-bg-tint-tokens.md new file mode 100644 index 0000000000..f1e3857744 --- /dev/null +++ b/.changeset/fix-table-bui-bg-tint-tokens.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Fixed Table component to use current `--bui-bg-neutral-1` tokens instead of the removed `--bui-bg-tint` tokens, restoring row hover, selected, pressed, and disabled background colors. diff --git a/packages/ui/src/components/Table/Table.module.css b/packages/ui/src/components/Table/Table.module.css index 55cf235e56..ccd8b6e47b 100644 --- a/packages/ui/src/components/Table/Table.module.css +++ b/packages/ui/src/components/Table/Table.module.css @@ -93,15 +93,15 @@ 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], @@ -111,7 +111,7 @@ } &[data-disabled] { - background-color: var(--bui-bg-tint-disabled); + background-color: var(--bui-bg-neutral-1-disabled); cursor: not-allowed; } } From e26e3de81e1ab39f24ea64ac2bb724d3e3935fdb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 11:07:36 +0100 Subject: [PATCH 141/188] frontend-plugin-api: accept IconElement in AuthProviderInfo Update the `icon` field on `AuthProviderInfo` to accept `IconElement` in addition to `IconComponent`, and update consumers in core-components and user-settings to handle both types at runtime. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- ...auth-provider-icon-element-core-components.md | 5 +++++ .../auth-provider-icon-element-user-settings.md | 5 +++++ .changeset/auth-provider-icon-element.md | 5 +++++ .../OAuthRequestDialog/LoginRequestListItem.tsx | 14 +++++++++----- packages/frontend-plugin-api/report.api.md | 2 +- .../src/apis/definitions/auth.ts | 9 +++++++-- plugins/user-settings/report.api.md | 3 ++- .../AuthProviders/ProviderSettingsItem.tsx | 16 ++++++++++------ 8 files changed, 44 insertions(+), 15 deletions(-) create mode 100644 .changeset/auth-provider-icon-element-core-components.md create mode 100644 .changeset/auth-provider-icon-element-user-settings.md create mode 100644 .changeset/auth-provider-icon-element.md 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/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 ?? <>} `) 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/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/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} Date: Mon, 16 Mar 2026 10:14:14 +0000 Subject: [PATCH 142/188] Update fix-header-container-padding.md Signed-off-by: Charles de Dreuille --- .changeset/fix-header-container-padding.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/fix-header-container-padding.md b/.changeset/fix-header-container-padding.md index fdad424ef5..cfb3611b8c 100644 --- a/.changeset/fix-header-container-padding.md +++ b/.changeset/fix-header-container-padding.md @@ -3,3 +3,5 @@ --- Fixed incorrect bottom spacing caused by `Container` using `padding-bottom` for its default bottom spacing. Changed to `margin-bottom` and prevented it from applying when `Container` is used as the `Header` root element. + +**Affected components:** Container, Header From 9508514116fa0308f82f43e3a4947b5c4cf9d358 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 11:19:57 +0100 Subject: [PATCH 143/188] frontend-plugin-api: promote PluginWrapper API to stable with useWrapperValue hook Promote PluginWrapperApi, pluginWrapperApiRef, PluginWrapperBlueprint, and the new PluginWrapperDefinition type from @alpha to @public. Add getRootWrapper() method to PluginWrapperApi and integrate the useWrapperValue hook pattern from the rugvip/plugin-wrapper branch, allowing plugin wrappers to share stateful values via a hook that runs once in the root wrapper and is distributed to all wrapper instances via useSyncExternalStore. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/promote-plugin-wrapper-api.md | 9 + .changeset/promote-plugin-wrapper-app.md | 5 + .../frontend-plugin-api/report-alpha.api.md | 44 ++-- packages/frontend-plugin-api/report.api.md | 49 +++++ packages/frontend-plugin-api/src/alpha.ts | 7 +- .../src/apis/definitions/PluginWrapperApi.ts | 26 ++- .../src/apis/definitions/index.ts | 1 + .../src/blueprints/PluginWrapperBlueprint.tsx | 42 +++- .../src/blueprints/index.ts | 4 + .../src/components/ExtensionBoundary.test.tsx | 8 + plugins/app/report.api.md | 7 +- .../DefaultPluginWrapperApi.test.tsx | 189 +++++++++++++++- .../DefaultPluginWrapperApi.tsx | 206 ++++++++++++++++-- plugins/app/src/extensions/AppRoot.tsx | 7 + .../app/src/extensions/PluginWrapperApi.ts | 2 - 15 files changed, 533 insertions(+), 73 deletions(-) create mode 100644 .changeset/promote-plugin-wrapper-api.md create mode 100644 .changeset/promote-plugin-wrapper-app.md diff --git a/.changeset/promote-plugin-wrapper-api.md b/.changeset/promote-plugin-wrapper-api.md new file mode 100644 index 0000000000..b9c6af8589 --- /dev/null +++ b/.changeset/promote-plugin-wrapper-api.md @@ -0,0 +1,9 @@ +--- +'@backstage/frontend-plugin-api': minor +--- + +**BREAKING**: Promoted `PluginWrapperApi`, `pluginWrapperApiRef`, `PluginWrapperBlueprint`, and the new `PluginWrapperDefinition` type from `@alpha` to `@public`. These are now available from the main package entry point rather than only through `/alpha`. + +The `PluginWrapperApi` type now has a required `getRootWrapper()` method that returns a root wrapper component. The `pluginWrapperApiRef` ID changed from `core.plugin-wrapper.alpha` to `core.plugin-wrapper`. + +The `PluginWrapperBlueprint` now accepts `PluginWrapperDefinition` as the loader return type, which supports an optional `useWrapperValue` hook that allows sharing state between wrapper instances. diff --git a/.changeset/promote-plugin-wrapper-app.md b/.changeset/promote-plugin-wrapper-app.md new file mode 100644 index 0000000000..e76d1247b0 --- /dev/null +++ b/.changeset/promote-plugin-wrapper-app.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app': patch +--- + +Updated the default `PluginWrapperApi` implementation to support the new `useWrapperValue` hook and root wrapper. The root wrapper is now rendered in the app root to manage shared hook state across plugin wrapper instances. 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 4624e68dda..902f6520dc 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -1880,6 +1880,55 @@ export interface PluginOptions< 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; 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/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/index.ts b/packages/frontend-plugin-api/src/apis/definitions/index.ts index 06d96a50a3..582eacf345 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/index.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/index.ts @@ -50,3 +50,4 @@ export * from './StorageApi'; export * from './AnalyticsApi'; export * from './TranslationApi'; export * from './PluginHeaderActionsApi'; +export * from './PluginWrapperApi'; 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/index.ts b/packages/frontend-plugin-api/src/blueprints/index.ts index d413776419..a571f6ef73 100644 --- a/packages/frontend-plugin-api/src/blueprints/index.ts +++ b/packages/frontend-plugin-api/src/blueprints/index.ts @@ -24,3 +24,7 @@ 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/plugins/app/report.api.md b/plugins/app/report.api.md index b1d851dab9..0262c4c1ef 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -19,6 +19,7 @@ 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'; @@ -620,11 +621,7 @@ const appPlugin: OverridableFrontendPlugin< inputs: { wrappers: ExtensionInput< ConfigurableExtensionDataRef< - () => Promise<{ - component: ComponentType<{ - children: ReactNode; - }>; - }>, + () => Promise, 'core.plugin-wrapper.loader', {} >, 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/AppRoot.tsx b/plugins/app/src/extensions/AppRoot.tsx index e43e4b46e4..2e49127a61 100644 --- a/plugins/app/src/extensions/AppRoot.tsx +++ b/plugins/app/src/extensions/AppRoot.tsx @@ -29,6 +29,7 @@ import { createExtension, createExtensionInput, routeResolutionApiRef, + pluginWrapperApiRef, useAnalytics, } from '@backstage/frontend-plugin-api'; import { @@ -115,6 +116,12 @@ export const AppRoot = createExtension({ } } + const pluginWrapperApi = apis.get(pluginWrapperApiRef); + const RootWrapper = pluginWrapperApi?.getRootWrapper(); + if (RootWrapper) { + content = {content}; + } + return [ coreExtensionData.reactElement( 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'; From 72991a5211bca7182d660689373527979821e4b2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 11:21:13 +0100 Subject: [PATCH 144/188] frontend-plugin-api: remove helper types from top-level API surface Remove `ResolvedExtensionInput` and `ExtensionDataRefToValue` from the public API to reduce top-level API clutter. These types were internal plumbing not needed by plugin authors. `ResolvedExtensionInput` is now a file-local type only used by `ResolvedExtensionInputs`. `ExtensionDataRefToValue` is no longer re-exported from the barrel. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/mdowryepwlkfidia.md | 5 +++++ .../src/tree/instantiateAppNodeTree.test.ts | 7 +++---- packages/frontend-plugin-api/report.api.md | 14 -------------- .../src/wiring/createExtension.ts | 7 ++----- .../src/wiring/createExtensionDataRef.ts | 2 +- packages/frontend-plugin-api/src/wiring/index.ts | 2 -- .../src/wiring/resolveInputOverrides.ts | 5 ++--- 7 files changed, 13 insertions(+), 29 deletions(-) create mode 100644 .changeset/mdowryepwlkfidia.md diff --git a/.changeset/mdowryepwlkfidia.md b/.changeset/mdowryepwlkfidia.md new file mode 100644 index 0000000000..f776d7f820 --- /dev/null +++ b/.changeset/mdowryepwlkfidia.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': minor +--- + +Removed the `ResolvedExtensionInput` and `ExtensionDataRefToValue` helper types from the public API surface to reduce top-level API clutter. These types were internal plumbing that are not needed by plugin authors. If you were relying on `ResolvedExtensionInput`, use the `ResolvedExtensionInputs` type instead, which maps a full set of inputs. If you were using `ExtensionDataRefToValue`, replace it with `ExtensionDataValue` combined with inferred types from your `ExtensionDataRef`. diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index fcef41daec..e52bbfa320 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -17,11 +17,10 @@ import { AppNode, Extension, + ExtensionDataContainer, ExtensionDataRef, ExtensionDefinition, - ExtensionInput, PortableSchema, - ResolvedExtensionInput, createExtension, createExtensionBlueprint, createExtensionDataRef, @@ -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-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index 4624e68dda..dc50c618b4 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -1156,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'; @@ -1907,14 +1901,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 { 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 27b12deb84..8ae176d249 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts @@ -33,7 +33,7 @@ export type ExtensionDataRef< readonly config: TConfig; }; -/** @public */ +/** @ignore */ export type ExtensionDataRefToValue = TDataRef extends ExtensionDataRef ? ExtensionDataValue diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index 044bf03b67..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 { @@ -32,7 +31,6 @@ export { export { createExtensionDataRef, type ExtensionDataRef, - type ExtensionDataRefToValue, type ExtensionDataValue, type ConfigurableExtensionDataRef, } from './createExtensionDataRef'; 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) { From 26eab3bf83d9427ecadecea5d0628c39b61c0590 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 12:27:08 +0100 Subject: [PATCH 145/188] Rename CLI module bin entries to use backstage prefix Rename all bin entries from `cli-module-*` to `backstage-cli-module-*` to establish a clear namespace for Backstage CLI tooling. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- ...-module-auth => backstage-cli-module-auth} | 0 packages/cli-module-auth/cli-report.md | 16 ++++++------- packages/cli-module-auth/package.json | 2 +- ...odule-build => backstage-cli-module-build} | 0 packages/cli-module-build/cli-report.md | 24 +++++++++---------- packages/cli-module-build/package.json | 2 +- ...ule-config => backstage-cli-module-config} | 0 packages/cli-module-config/cli-report.md | 16 ++++++------- packages/cli-module-config/package.json | 2 +- ...=> backstage-cli-module-create-github-app} | 0 .../cli-report.md | 4 ++-- .../cli-module-create-github-app/package.json | 2 +- ...-module-info => backstage-cli-module-info} | 0 packages/cli-module-info/cli-report.md | 4 ++-- packages/cli-module-info/package.json | 2 +- ...-module-lint => backstage-cli-module-lint} | 0 packages/cli-module-lint/cli-report.md | 10 ++++---- packages/cli-module-lint/package.json | 2 +- ...nance => backstage-cli-module-maintenance} | 0 packages/cli-module-maintenance/cli-report.md | 8 +++---- packages/cli-module-maintenance/package.json | 2 +- ...e-migrate => backstage-cli-module-migrate} | 0 packages/cli-module-migrate/cli-report.md | 18 +++++++------- packages/cli-module-migrate/package.json | 2 +- ...li-module-new => backstage-cli-module-new} | 0 packages/cli-module-new/cli-report.md | 4 ++-- packages/cli-module-new/package.json | 2 +- ...st-jest => backstage-cli-module-test-jest} | 0 packages/cli-module-test-jest/cli-report.md | 12 +++++----- packages/cli-module-test-jest/package.json | 2 +- ...ions => backstage-cli-module-translations} | 0 .../cli-module-translations/cli-report.md | 8 +++---- packages/cli-module-translations/package.json | 2 +- yarn.lock | 22 ++++++++--------- 34 files changed, 84 insertions(+), 84 deletions(-) rename packages/cli-module-auth/bin/{cli-module-auth => backstage-cli-module-auth} (100%) rename packages/cli-module-build/bin/{cli-module-build => backstage-cli-module-build} (100%) rename packages/cli-module-config/bin/{cli-module-config => backstage-cli-module-config} (100%) rename packages/cli-module-create-github-app/bin/{cli-module-create-github-app => backstage-cli-module-create-github-app} (100%) rename packages/cli-module-info/bin/{cli-module-info => backstage-cli-module-info} (100%) rename packages/cli-module-lint/bin/{cli-module-lint => backstage-cli-module-lint} (100%) rename packages/cli-module-maintenance/bin/{cli-module-maintenance => backstage-cli-module-maintenance} (100%) rename packages/cli-module-migrate/bin/{cli-module-migrate => backstage-cli-module-migrate} (100%) rename packages/cli-module-new/bin/{cli-module-new => backstage-cli-module-new} (100%) rename packages/cli-module-test-jest/bin/{cli-module-test-jest => backstage-cli-module-test-jest} (100%) rename packages/cli-module-translations/bin/{cli-module-translations => backstage-cli-module-translations} (100%) diff --git a/packages/cli-module-auth/bin/cli-module-auth b/packages/cli-module-auth/bin/backstage-cli-module-auth similarity index 100% rename from packages/cli-module-auth/bin/cli-module-auth rename to packages/cli-module-auth/bin/backstage-cli-module-auth diff --git a/packages/cli-module-auth/cli-report.md b/packages/cli-module-auth/cli-report.md index 1b382040a2..8b4c98766f 100644 --- a/packages/cli-module-auth/cli-report.md +++ b/packages/cli-module-auth/cli-report.md @@ -2,7 +2,7 @@ > Do not edit this file. It is a report generated by `yarn build:api-reports` -### `cli-module-auth` +### `backstage-cli-module-auth` ``` Usage: @backstage/cli-module-auth [options] [command] @@ -16,7 +16,7 @@ Commands: help [command] ``` -### `cli-module-auth auth` +### `backstage-cli-module-auth auth` ``` Usage: @backstage/cli-module-auth auth [options] [command] [command] @@ -34,7 +34,7 @@ Commands: show ``` -### `cli-module-auth auth list` +### `backstage-cli-module-auth auth list` ``` Usage: @backstage/cli-module-auth auth list @@ -43,7 +43,7 @@ Options: -h, --help ``` -### `cli-module-auth auth login` +### `backstage-cli-module-auth auth login` ``` Usage: @backstage/cli-module-auth auth login @@ -55,7 +55,7 @@ Options: -h, --help ``` -### `cli-module-auth auth logout` +### `backstage-cli-module-auth auth logout` ``` Usage: @backstage/cli-module-auth auth logout @@ -65,7 +65,7 @@ Options: -h, --help ``` -### `cli-module-auth auth print-token` +### `backstage-cli-module-auth auth print-token` ``` Usage: @backstage/cli-module-auth auth print-token @@ -75,7 +75,7 @@ Options: -h, --help ``` -### `cli-module-auth auth select` +### `backstage-cli-module-auth auth select` ``` Usage: @backstage/cli-module-auth auth select @@ -85,7 +85,7 @@ Options: -h, --help ``` -### `cli-module-auth auth show` +### `backstage-cli-module-auth auth show` ``` Usage: @backstage/cli-module-auth auth show diff --git a/packages/cli-module-auth/package.json b/packages/cli-module-auth/package.json index c3e2d47224..86bb491f7a 100644 --- a/packages/cli-module-auth/package.json +++ b/packages/cli-module-auth/package.json @@ -52,5 +52,5 @@ "optionalDependencies": { "keytar": "^7.9.0" }, - "bin": "bin/cli-module-auth" + "bin": "bin/backstage-cli-module-auth" } diff --git a/packages/cli-module-build/bin/cli-module-build b/packages/cli-module-build/bin/backstage-cli-module-build similarity index 100% rename from packages/cli-module-build/bin/cli-module-build rename to packages/cli-module-build/bin/backstage-cli-module-build diff --git a/packages/cli-module-build/cli-report.md b/packages/cli-module-build/cli-report.md index b2240e0523..1c11022dc8 100644 --- a/packages/cli-module-build/cli-report.md +++ b/packages/cli-module-build/cli-report.md @@ -2,7 +2,7 @@ > Do not edit this file. It is a report generated by `yarn build:api-reports` -### `cli-module-build` +### `backstage-cli-module-build` ``` Usage: @backstage/cli-module-build [options] [command] @@ -18,7 +18,7 @@ Commands: repo [command] ``` -### `cli-module-build build-workspace` +### `backstage-cli-module-build build-workspace` ``` Usage: @backstage/cli-module-build build-workspace [packages...] @@ -28,7 +28,7 @@ Options: -h, --help ``` -### `cli-module-build package` +### `backstage-cli-module-build package` ``` Usage: @backstage/cli-module-build package [options] [command] [command] @@ -45,7 +45,7 @@ Commands: start ``` -### `cli-module-build package build` +### `backstage-cli-module-build package build` ``` Usage: @backstage/cli-module-build package build @@ -60,7 +60,7 @@ Options: -h, --help ``` -### `cli-module-build package clean` +### `backstage-cli-module-build package clean` ``` Usage: @backstage/cli-module-build package clean @@ -69,7 +69,7 @@ Options: -h, --help ``` -### `cli-module-build package postpack` +### `backstage-cli-module-build package postpack` ``` Usage: @backstage/cli-module-build package postpack @@ -78,7 +78,7 @@ Options: -h, --help ``` -### `cli-module-build package prepack` +### `backstage-cli-module-build package prepack` ``` Usage: @backstage/cli-module-build package prepack @@ -87,7 +87,7 @@ Options: -h, --help ``` -### `cli-module-build package start` +### `backstage-cli-module-build package start` ``` Usage: @backstage/cli-module-build package start @@ -104,7 +104,7 @@ Options: -h, --help ``` -### `cli-module-build repo` +### `backstage-cli-module-build repo` ``` Usage: @backstage/cli-module-build repo [options] [command] [command] @@ -119,7 +119,7 @@ Commands: start ``` -### `cli-module-build repo build` +### `backstage-cli-module-build repo build` ``` Usage: @backstage/cli-module-build repo build @@ -131,7 +131,7 @@ Options: -h, --help ``` -### `cli-module-build repo clean` +### `backstage-cli-module-build repo clean` ``` Usage: @backstage/cli-module-build repo clean @@ -140,7 +140,7 @@ Options: -h, --help ``` -### `cli-module-build repo start` +### `backstage-cli-module-build repo start` ``` Usage: @backstage/cli-module-build repo start [packages...] diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index 519b24b701..27f4fd3903 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -19,7 +19,7 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", - "bin": "bin/cli-module-build", + "bin": "bin/backstage-cli-module-build", "files": [ "dist", "bin", diff --git a/packages/cli-module-config/bin/cli-module-config b/packages/cli-module-config/bin/backstage-cli-module-config similarity index 100% rename from packages/cli-module-config/bin/cli-module-config rename to packages/cli-module-config/bin/backstage-cli-module-config diff --git a/packages/cli-module-config/cli-report.md b/packages/cli-module-config/cli-report.md index 6cb8a15d62..953c347951 100644 --- a/packages/cli-module-config/cli-report.md +++ b/packages/cli-module-config/cli-report.md @@ -2,7 +2,7 @@ > Do not edit this file. It is a report generated by `yarn build:api-reports` -### `cli-module-config` +### `backstage-cli-module-config` ``` Usage: @backstage/cli-module-config [options] [command] @@ -20,7 +20,7 @@ Commands: help [command] ``` -### `cli-module-config config` +### `backstage-cli-module-config config` ``` Usage: @backstage/cli-module-config config [options] [command] [command] @@ -34,7 +34,7 @@ Commands: schema ``` -### `cli-module-config config docs` +### `backstage-cli-module-config config docs` ``` Usage: @backstage/cli-module-config config docs @@ -44,7 +44,7 @@ Options: -h, --help ``` -### `cli-module-config config schema` +### `backstage-cli-module-config config schema` ``` Usage: @backstage/cli-module-config config schema @@ -56,7 +56,7 @@ Options: -h, --help ``` -### `cli-module-config config:check` +### `backstage-cli-module-config config:check` ``` Usage: @backstage/cli-module-config config:check @@ -71,7 +71,7 @@ Options: -h, --help ``` -### `cli-module-config config:docs` +### `backstage-cli-module-config config:docs` ``` Usage: @backstage/cli-module-config config:docs @@ -81,7 +81,7 @@ Options: -h, --help ``` -### `cli-module-config config:print` +### `backstage-cli-module-config config:print` ``` Usage: @backstage/cli-module-config config:print @@ -96,7 +96,7 @@ Options: -h, --help ``` -### `cli-module-config config:schema` +### `backstage-cli-module-config config:schema` ``` Usage: @backstage/cli-module-config config:schema diff --git a/packages/cli-module-config/package.json b/packages/cli-module-config/package.json index 4c281dcaa3..bbdf6070a3 100644 --- a/packages/cli-module-config/package.json +++ b/packages/cli-module-config/package.json @@ -48,5 +48,5 @@ "@backstage/cli": "workspace:^", "@types/json-schema": "^7.0.6" }, - "bin": "bin/cli-module-config" + "bin": "bin/backstage-cli-module-config" } diff --git a/packages/cli-module-create-github-app/bin/cli-module-create-github-app b/packages/cli-module-create-github-app/bin/backstage-cli-module-create-github-app similarity index 100% rename from packages/cli-module-create-github-app/bin/cli-module-create-github-app rename to packages/cli-module-create-github-app/bin/backstage-cli-module-create-github-app diff --git a/packages/cli-module-create-github-app/cli-report.md b/packages/cli-module-create-github-app/cli-report.md index a12ea68bf2..7a7f7bb3a2 100644 --- a/packages/cli-module-create-github-app/cli-report.md +++ b/packages/cli-module-create-github-app/cli-report.md @@ -2,7 +2,7 @@ > Do not edit this file. It is a report generated by `yarn build:api-reports` -### `cli-module-create-github-app` +### `backstage-cli-module-create-github-app` ``` Usage: @backstage/cli-module-create-github-app [options] [command] @@ -16,7 +16,7 @@ Commands: help [command] ``` -### `cli-module-create-github-app create-github-app` +### `backstage-cli-module-create-github-app create-github-app` ``` Usage: @backstage/cli-module-create-github-app create-github-app diff --git a/packages/cli-module-create-github-app/package.json b/packages/cli-module-create-github-app/package.json index 9b342a4551..2196abc62e 100644 --- a/packages/cli-module-create-github-app/package.json +++ b/packages/cli-module-create-github-app/package.json @@ -48,5 +48,5 @@ "@types/express": "^4.17.6", "@types/fs-extra": "^11.0.0" }, - "bin": "bin/cli-module-create-github-app" + "bin": "bin/backstage-cli-module-create-github-app" } diff --git a/packages/cli-module-info/bin/cli-module-info b/packages/cli-module-info/bin/backstage-cli-module-info similarity index 100% rename from packages/cli-module-info/bin/cli-module-info rename to packages/cli-module-info/bin/backstage-cli-module-info diff --git a/packages/cli-module-info/cli-report.md b/packages/cli-module-info/cli-report.md index f292e46a18..f83583b781 100644 --- a/packages/cli-module-info/cli-report.md +++ b/packages/cli-module-info/cli-report.md @@ -2,7 +2,7 @@ > Do not edit this file. It is a report generated by `yarn build:api-reports` -### `cli-module-info` +### `backstage-cli-module-info` ``` Usage: @backstage/cli-module-info [options] [command] @@ -16,7 +16,7 @@ Commands: info ``` -### `cli-module-info info` +### `backstage-cli-module-info info` ``` Usage: @backstage/cli-module-info info diff --git a/packages/cli-module-info/package.json b/packages/cli-module-info/package.json index aafa7908fe..db19b358f4 100644 --- a/packages/cli-module-info/package.json +++ b/packages/cli-module-info/package.json @@ -42,5 +42,5 @@ "@backstage/cli": "workspace:^", "@types/fs-extra": "^11.0.0" }, - "bin": "bin/cli-module-info" + "bin": "bin/backstage-cli-module-info" } diff --git a/packages/cli-module-lint/bin/cli-module-lint b/packages/cli-module-lint/bin/backstage-cli-module-lint similarity index 100% rename from packages/cli-module-lint/bin/cli-module-lint rename to packages/cli-module-lint/bin/backstage-cli-module-lint diff --git a/packages/cli-module-lint/cli-report.md b/packages/cli-module-lint/cli-report.md index c577759e0c..2a9ff6d61f 100644 --- a/packages/cli-module-lint/cli-report.md +++ b/packages/cli-module-lint/cli-report.md @@ -2,7 +2,7 @@ > Do not edit this file. It is a report generated by `yarn build:api-reports` -### `cli-module-lint` +### `backstage-cli-module-lint` ``` Usage: @backstage/cli-module-lint [options] [command] @@ -17,7 +17,7 @@ Commands: repo [command] ``` -### `cli-module-lint package` +### `backstage-cli-module-lint package` ``` Usage: @backstage/cli-module-lint package [options] [command] [command] @@ -30,7 +30,7 @@ Commands: lint ``` -### `cli-module-lint package lint` +### `backstage-cli-module-lint package lint` ``` Usage: @backstage/cli-module-lint package lint [directories...] @@ -43,7 +43,7 @@ Options: -h, --help ``` -### `cli-module-lint repo` +### `backstage-cli-module-lint repo` ``` Usage: @backstage/cli-module-lint repo [options] [command] [command] @@ -56,7 +56,7 @@ Commands: lint ``` -### `cli-module-lint repo lint` +### `backstage-cli-module-lint repo lint` ``` Usage: @backstage/cli-module-lint repo lint diff --git a/packages/cli-module-lint/package.json b/packages/cli-module-lint/package.json index e6b9615e39..452e10eeed 100644 --- a/packages/cli-module-lint/package.json +++ b/packages/cli-module-lint/package.json @@ -47,5 +47,5 @@ "@types/fs-extra": "^11.0.0", "@types/shell-quote": "^1.7.5" }, - "bin": "bin/cli-module-lint" + "bin": "bin/backstage-cli-module-lint" } diff --git a/packages/cli-module-maintenance/bin/cli-module-maintenance b/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance similarity index 100% rename from packages/cli-module-maintenance/bin/cli-module-maintenance rename to packages/cli-module-maintenance/bin/backstage-cli-module-maintenance diff --git a/packages/cli-module-maintenance/cli-report.md b/packages/cli-module-maintenance/cli-report.md index 66b9950328..874d9d781d 100644 --- a/packages/cli-module-maintenance/cli-report.md +++ b/packages/cli-module-maintenance/cli-report.md @@ -2,7 +2,7 @@ > Do not edit this file. It is a report generated by `yarn build:api-reports` -### `cli-module-maintenance` +### `backstage-cli-module-maintenance` ``` Usage: @backstage/cli-module-maintenance [options] [command] @@ -16,7 +16,7 @@ Commands: repo [command] ``` -### `cli-module-maintenance repo` +### `backstage-cli-module-maintenance repo` ``` Usage: @backstage/cli-module-maintenance repo [options] [command] [command] @@ -30,7 +30,7 @@ Commands: list-deprecations ``` -### `cli-module-maintenance repo fix` +### `backstage-cli-module-maintenance repo fix` ``` Usage: @backstage/cli-module-maintenance repo fix @@ -41,7 +41,7 @@ Options: -h, --help ``` -### `cli-module-maintenance repo list-deprecations` +### `backstage-cli-module-maintenance repo list-deprecations` ``` Usage: @backstage/cli-module-maintenance repo list-deprecations diff --git a/packages/cli-module-maintenance/package.json b/packages/cli-module-maintenance/package.json index 4293beb4ff..24f7a9b70a 100644 --- a/packages/cli-module-maintenance/package.json +++ b/packages/cli-module-maintenance/package.json @@ -43,5 +43,5 @@ "@backstage/cli": "workspace:^", "@types/fs-extra": "^11.0.0" }, - "bin": "bin/cli-module-maintenance" + "bin": "bin/backstage-cli-module-maintenance" } diff --git a/packages/cli-module-migrate/bin/cli-module-migrate b/packages/cli-module-migrate/bin/backstage-cli-module-migrate similarity index 100% rename from packages/cli-module-migrate/bin/cli-module-migrate rename to packages/cli-module-migrate/bin/backstage-cli-module-migrate diff --git a/packages/cli-module-migrate/cli-report.md b/packages/cli-module-migrate/cli-report.md index 06a60cdfaf..f0c29e1edd 100644 --- a/packages/cli-module-migrate/cli-report.md +++ b/packages/cli-module-migrate/cli-report.md @@ -2,7 +2,7 @@ > Do not edit this file. It is a report generated by `yarn build:api-reports` -### `cli-module-migrate` +### `backstage-cli-module-migrate` ``` Usage: @backstage/cli-module-migrate [options] [command] @@ -18,7 +18,7 @@ Commands: versions:migrate ``` -### `cli-module-migrate migrate` +### `backstage-cli-module-migrate migrate` ``` Usage: @backstage/cli-module-migrate migrate [options] [command] [command] @@ -35,7 +35,7 @@ Commands: react-router-deps ``` -### `cli-module-migrate migrate package-exports` +### `backstage-cli-module-migrate migrate package-exports` ``` Usage: @backstage/cli-module-migrate migrate package-exports @@ -44,7 +44,7 @@ Options: -h, --help ``` -### `cli-module-migrate migrate package-lint-configs` +### `backstage-cli-module-migrate migrate package-lint-configs` ``` Usage: @backstage/cli-module-migrate migrate package-lint-configs @@ -53,7 +53,7 @@ Options: -h, --help ``` -### `cli-module-migrate migrate package-roles` +### `backstage-cli-module-migrate migrate package-roles` ``` Usage: @backstage/cli-module-migrate migrate package-roles @@ -62,7 +62,7 @@ Options: -h, --help ``` -### `cli-module-migrate migrate package-scripts` +### `backstage-cli-module-migrate migrate package-scripts` ``` Usage: @backstage/cli-module-migrate migrate package-scripts @@ -71,7 +71,7 @@ Options: -h, --help ``` -### `cli-module-migrate migrate react-router-deps` +### `backstage-cli-module-migrate migrate react-router-deps` ``` Usage: @backstage/cli-module-migrate migrate react-router-deps @@ -80,7 +80,7 @@ Options: -h, --help ``` -### `cli-module-migrate versions:bump` +### `backstage-cli-module-migrate versions:bump` ``` Usage: @backstage/cli-module-migrate versions:bump @@ -93,7 +93,7 @@ Options: -h, --help ``` -### `cli-module-migrate versions:migrate` +### `backstage-cli-module-migrate versions:migrate` ``` Usage: @backstage/cli-module-migrate versions:migrate diff --git a/packages/cli-module-migrate/package.json b/packages/cli-module-migrate/package.json index b558cfab19..dfc155fd11 100644 --- a/packages/cli-module-migrate/package.json +++ b/packages/cli-module-migrate/package.json @@ -53,5 +53,5 @@ "@types/semver": "^7", "msw": "^1.0.0" }, - "bin": "bin/cli-module-migrate" + "bin": "bin/backstage-cli-module-migrate" } diff --git a/packages/cli-module-new/bin/cli-module-new b/packages/cli-module-new/bin/backstage-cli-module-new similarity index 100% rename from packages/cli-module-new/bin/cli-module-new rename to packages/cli-module-new/bin/backstage-cli-module-new diff --git a/packages/cli-module-new/cli-report.md b/packages/cli-module-new/cli-report.md index 2c414fc747..a2b4768148 100644 --- a/packages/cli-module-new/cli-report.md +++ b/packages/cli-module-new/cli-report.md @@ -2,7 +2,7 @@ > Do not edit this file. It is a report generated by `yarn build:api-reports` -### `cli-module-new` +### `backstage-cli-module-new` ``` Usage: @backstage/cli-module-new [options] [command] @@ -16,7 +16,7 @@ Commands: new ``` -### `cli-module-new new` +### `backstage-cli-module-new new` ``` Usage: @backstage/cli-module-new new diff --git a/packages/cli-module-new/package.json b/packages/cli-module-new/package.json index bd056c507c..9ca1664f7a 100644 --- a/packages/cli-module-new/package.json +++ b/packages/cli-module-new/package.json @@ -58,5 +58,5 @@ "@types/lodash": "^4.14.151", "@types/recursive-readdir": "^2.2.0" }, - "bin": "bin/cli-module-new" + "bin": "bin/backstage-cli-module-new" } diff --git a/packages/cli-module-test-jest/bin/cli-module-test-jest b/packages/cli-module-test-jest/bin/backstage-cli-module-test-jest similarity index 100% rename from packages/cli-module-test-jest/bin/cli-module-test-jest rename to packages/cli-module-test-jest/bin/backstage-cli-module-test-jest diff --git a/packages/cli-module-test-jest/cli-report.md b/packages/cli-module-test-jest/cli-report.md index ca1d390ed7..00523eb8cf 100644 --- a/packages/cli-module-test-jest/cli-report.md +++ b/packages/cli-module-test-jest/cli-report.md @@ -2,7 +2,7 @@ > Do not edit this file. It is a report generated by `yarn build:api-reports` -### `cli-module-test-jest` +### `backstage-cli-module-test-jest` ``` Usage: @backstage/cli-module-test-jest [options] [command] @@ -17,7 +17,7 @@ Commands: repo [command] ``` -### `cli-module-test-jest package` +### `backstage-cli-module-test-jest package` ``` Usage: @backstage/cli-module-test-jest package [options] [command] [command] @@ -30,10 +30,10 @@ Commands: test ``` -### `cli-module-test-jest package test` +### `backstage-cli-module-test-jest package test` ``` -Usage: cli-module-test-jest [--config=] [TestPathPatterns] +Usage: backstage-cli-module-test-jest [--config=] Options: --all @@ -143,7 +143,7 @@ Options: -w, --maxWorkers ``` -### `cli-module-test-jest repo` +### `backstage-cli-module-test-jest repo` ``` Usage: @backstage/cli-module-test-jest repo [options] [command] [command] @@ -156,7 +156,7 @@ Commands: test ``` -### `cli-module-test-jest repo test` +### `backstage-cli-module-test-jest repo test` ``` Usage: @backstage/cli-module-test-jest repo test diff --git a/packages/cli-module-test-jest/package.json b/packages/cli-module-test-jest/package.json index 36dd8fafdf..6d7811f484 100644 --- a/packages/cli-module-test-jest/package.json +++ b/packages/cli-module-test-jest/package.json @@ -65,5 +65,5 @@ "optional": true } }, - "bin": "bin/cli-module-test-jest" + "bin": "bin/backstage-cli-module-test-jest" } diff --git a/packages/cli-module-translations/bin/cli-module-translations b/packages/cli-module-translations/bin/backstage-cli-module-translations similarity index 100% rename from packages/cli-module-translations/bin/cli-module-translations rename to packages/cli-module-translations/bin/backstage-cli-module-translations diff --git a/packages/cli-module-translations/cli-report.md b/packages/cli-module-translations/cli-report.md index 9b2d5ef3b4..bf2ff28001 100644 --- a/packages/cli-module-translations/cli-report.md +++ b/packages/cli-module-translations/cli-report.md @@ -2,7 +2,7 @@ > Do not edit this file. It is a report generated by `yarn build:api-reports` -### `cli-module-translations` +### `backstage-cli-module-translations` ``` Usage: @backstage/cli-module-translations [options] [command] @@ -16,7 +16,7 @@ Commands: translations [command] ``` -### `cli-module-translations translations` +### `backstage-cli-module-translations translations` ``` Usage: @backstage/cli-module-translations translations [options] [command] [command] @@ -30,7 +30,7 @@ Commands: import ``` -### `cli-module-translations translations export` +### `backstage-cli-module-translations translations export` ``` Usage: @backstage/cli-module-translations translations export @@ -41,7 +41,7 @@ Options: -h, --help ``` -### `cli-module-translations translations import` +### `backstage-cli-module-translations translations import` ``` Usage: @backstage/cli-module-translations translations import diff --git a/packages/cli-module-translations/package.json b/packages/cli-module-translations/package.json index 0bb3714889..86ef2b7ca4 100644 --- a/packages/cli-module-translations/package.json +++ b/packages/cli-module-translations/package.json @@ -42,5 +42,5 @@ "@backstage/cli": "workspace:^", "@types/fs-extra": "^11.0.0" }, - "bin": "bin/cli-module-translations" + "bin": "bin/backstage-cli-module-translations" } diff --git a/yarn.lock b/yarn.lock index 275cf38895..1c4ba53f5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2843,7 +2843,7 @@ __metadata: keytar: optional: true bin: - cli-module-auth: bin/cli-module-auth + cli-module-auth: bin/backstage-cli-module-auth languageName: unknown linkType: soft @@ -2918,7 +2918,7 @@ __metadata: yml-loader: "npm:^2.1.0" yn: "npm:^4.0.0" bin: - cli-module-build: bin/cli-module-build + cli-module-build: bin/backstage-cli-module-build languageName: unknown linkType: soft @@ -2940,7 +2940,7 @@ __metadata: react-dev-utils: "npm:^12.0.0-next.60" yaml: "npm:^2.0.0" bin: - cli-module-config: bin/cli-module-config + cli-module-config: bin/backstage-cli-module-config languageName: unknown linkType: soft @@ -2962,7 +2962,7 @@ __metadata: react-dev-utils: "npm:^12.0.0-next.60" yaml: "npm:^2.0.0" bin: - cli-module-create-github-app: bin/cli-module-create-github-app + cli-module-create-github-app: bin/backstage-cli-module-create-github-app languageName: unknown linkType: soft @@ -2978,7 +2978,7 @@ __metadata: fs-extra: "npm:^11.2.0" minimatch: "npm:^10.2.1" bin: - cli-module-info: bin/cli-module-info + cli-module-info: bin/backstage-cli-module-info languageName: unknown linkType: soft @@ -2999,7 +2999,7 @@ __metadata: globby: "npm:^11.1.0" shell-quote: "npm:^1.8.1" bin: - cli-module-lint: bin/cli-module-lint + cli-module-lint: bin/backstage-cli-module-lint languageName: unknown linkType: soft @@ -3016,7 +3016,7 @@ __metadata: eslint: "npm:^8.6.0" fs-extra: "npm:^11.2.0" bin: - cli-module-maintenance: bin/cli-module-maintenance + cli-module-maintenance: bin/backstage-cli-module-maintenance languageName: unknown linkType: soft @@ -3043,7 +3043,7 @@ __metadata: replace-in-file: "npm:^7.1.0" semver: "npm:^7.5.3" bin: - cli-module-migrate: bin/cli-module-migrate + cli-module-migrate: bin/backstage-cli-module-migrate languageName: unknown linkType: soft @@ -3075,7 +3075,7 @@ __metadata: zod: "npm:^3.25.76" zod-validation-error: "npm:^4.0.2" bin: - cli-module-new: bin/cli-module-new + cli-module-new: bin/backstage-cli-module-new languageName: unknown linkType: soft @@ -3108,7 +3108,7 @@ __metadata: jsdom: optional: true bin: - cli-module-test-jest: bin/cli-module-test-jest + cli-module-test-jest: bin/backstage-cli-module-test-jest languageName: unknown linkType: soft @@ -3124,7 +3124,7 @@ __metadata: fs-extra: "npm:^11.2.0" ts-morph: "npm:^24.0.0" bin: - cli-module-translations: bin/cli-module-translations + cli-module-translations: bin/backstage-cli-module-translations languageName: unknown linkType: soft From 4d081452b14eb297ef1c1c78576ee47ecc2784b6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 12:55:22 +0100 Subject: [PATCH 146/188] Address review feedback from freben - Use cli-defaults instead of listing individual CLI modules in create-app template and root package.json - Move nodeTransform config files from cli-module-build to cli-node to avoid cross-module direct imports - Rename cli-module-create-github-app to cli-module-github - Start createCliModule init chain with Promise.resolve() - Deduplicate exitWithError in runCliModule.ts - Extract shared isCommandNodeHidden to @internal/cli - Add explanatory comment for fromArray deduplication field - Restore error for cli role packages missing bin in runCliExtraction Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/cli-discover-modules.md | 2 +- .changeset/introduce-cli-modules.md | 2 +- package.json | 12 +---- packages/cli-defaults/README.md | 26 +++++------ packages/cli-defaults/package.json | 2 +- packages/cli-defaults/src/index.ts | 4 +- .../cli-internal/src/InternalCommandNode.ts | 14 ++++++ packages/cli-internal/src/index.ts | 1 + .../bin/backstage-cli-module-auth | 2 +- .../bin/backstage-cli-module-build | 2 +- packages/cli-module-build/package.json | 1 - .../src/lib/runner/runBackend.ts | 7 ++- .../src/tests/transforms/transforms.test.ts | 4 +- .../bin/backstage-cli-module-config | 2 +- .../cli-report.md | 26 ----------- .../.eslintrc.js | 0 .../README.md | 2 +- .../bin/backstage-cli-module-github} | 2 +- .../catalog-info.yaml | 4 +- packages/cli-module-github/cli-report.md | 26 +++++++++++ .../package.json | 6 +-- .../report.api.md | 2 +- .../src/cli.ts | 0 .../GithubCreateAppServer.ts | 0 .../src/commands/create-github-app/index.ts | 0 .../src/index.ts | 0 .../bin/backstage-cli-module-info | 2 +- .../bin/backstage-cli-module-lint | 2 +- .../bin/backstage-cli-module-maintenance | 2 +- .../bin/backstage-cli-module-migrate | 2 +- .../bin/backstage-cli-module-new | 2 +- .../bin/backstage-cli-module-test-jest | 2 +- .../bin/backstage-cli-module-translations | 2 +- packages/cli-node/.eslintrc.js | 20 ++++++++- .../config/nodeTransform.cjs | 0 .../config/nodeTransformHooks.mjs | 0 packages/cli-node/package.json | 5 ++- .../src/cli-module/createCliModule.ts | 4 +- .../cli-node/src/cli-module/runCliModule.ts | 20 +++------ packages/cli/config/nodeTransform.cjs | 4 +- packages/cli/config/nodeTransformHooks.mjs | 2 +- packages/cli/src/wiring/CliInitializer.ts | 19 ++++---- packages/create-app/src/lib/tasks.test.ts | 11 ----- packages/create-app/src/lib/versions.ts | 22 --------- .../templates/next-app/package.json.hbs | 12 +---- .../cli-reports/runCliExtraction.ts | 5 +++ yarn.lock | 45 ++++++++----------- 47 files changed, 147 insertions(+), 185 deletions(-) delete mode 100644 packages/cli-module-create-github-app/cli-report.md rename packages/{cli-module-create-github-app => cli-module-github}/.eslintrc.js (100%) rename packages/{cli-module-create-github-app => cli-module-github}/README.md (92%) rename packages/{cli-module-create-github-app/bin/backstage-cli-module-create-github-app => cli-module-github/bin/backstage-cli-module-github} (94%) rename packages/{cli-module-create-github-app => cli-module-github}/catalog-info.yaml (66%) create mode 100644 packages/cli-module-github/cli-report.md rename packages/{cli-module-create-github-app => cli-module-github}/package.json (88%) rename packages/{cli-module-create-github-app => cli-module-github}/report.api.md (81%) rename packages/{cli-module-create-github-app => cli-module-github}/src/cli.ts (100%) rename packages/{cli-module-create-github-app => cli-module-github}/src/commands/create-github-app/GithubCreateAppServer.ts (100%) rename packages/{cli-module-create-github-app => cli-module-github}/src/commands/create-github-app/index.ts (100%) rename packages/{cli-module-create-github-app => cli-module-github}/src/index.ts (100%) rename packages/{cli-module-build => cli-node}/config/nodeTransform.cjs (100%) rename packages/{cli-module-build => cli-node}/config/nodeTransformHooks.mjs (100%) diff --git a/.changeset/cli-discover-modules.md b/.changeset/cli-discover-modules.md index 751facdf1d..dac5f73f75 100644 --- a/.changeset/cli-discover-modules.md +++ b/.changeset/cli-discover-modules.md @@ -28,7 +28,7 @@ For fine-grained control you can instead install individual CLI modules: "@backstage/cli-module-auth": "backstage:^", "@backstage/cli-module-build": "backstage:^", "@backstage/cli-module-config": "backstage:^", - "@backstage/cli-module-create-github-app": "backstage:^", + "@backstage/cli-module-github": "backstage:^", "@backstage/cli-module-info": "backstage:^", "@backstage/cli-module-lint": "backstage:^", "@backstage/cli-module-maintenance": "backstage:^", diff --git a/.changeset/introduce-cli-modules.md b/.changeset/introduce-cli-modules.md index a69df9164b..7381742621 100644 --- a/.changeset/introduce-cli-modules.md +++ b/.changeset/introduce-cli-modules.md @@ -2,7 +2,7 @@ '@backstage/cli-module-auth': minor '@backstage/cli-module-build': minor '@backstage/cli-module-config': minor -'@backstage/cli-module-create-github-app': minor +'@backstage/cli-module-github': minor '@backstage/cli-module-info': minor '@backstage/cli-module-lint': minor '@backstage/cli-module-maintenance': minor diff --git a/package.json b/package.json index c443b8aa81..c89dcefce6 100644 --- a/package.json +++ b/package.json @@ -125,17 +125,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:*", - "@backstage/cli-module-auth": "workspace:*", - "@backstage/cli-module-build": "workspace:*", - "@backstage/cli-module-config": "workspace:*", - "@backstage/cli-module-create-github-app": "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:*", + "@backstage/cli-defaults": "workspace:*", "@backstage/codemods": "workspace:*", "@backstage/create-app": "workspace:*", "@backstage/e2e-test-utils": "workspace:*", diff --git a/packages/cli-defaults/README.md b/packages/cli-defaults/README.md index fa75c322df..2066aa1e35 100644 --- a/packages/cli-defaults/README.md +++ b/packages/cli-defaults/README.md @@ -4,19 +4,19 @@ The default set of CLI modules for the Backstage CLI. Installing this single pac ## 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-create-github-app`](../cli-module-create-github-app) | 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 | +| 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. diff --git a/packages/cli-defaults/package.json b/packages/cli-defaults/package.json index 587e54cfe9..8a065fd4d7 100644 --- a/packages/cli-defaults/package.json +++ b/packages/cli-defaults/package.json @@ -33,7 +33,7 @@ "@backstage/cli-module-auth": "workspace:^", "@backstage/cli-module-build": "workspace:^", "@backstage/cli-module-config": "workspace:^", - "@backstage/cli-module-create-github-app": "workspace:^", + "@backstage/cli-module-github": "workspace:^", "@backstage/cli-module-info": "workspace:^", "@backstage/cli-module-lint": "workspace:^", "@backstage/cli-module-maintenance": "workspace:^", diff --git a/packages/cli-defaults/src/index.ts b/packages/cli-defaults/src/index.ts index 0126d0c074..dd7aed0c70 100644 --- a/packages/cli-defaults/src/index.ts +++ b/packages/cli-defaults/src/index.ts @@ -16,7 +16,7 @@ import auth from '@backstage/cli-module-auth'; import build from '@backstage/cli-module-build'; import config from '@backstage/cli-module-config'; -import createGithubApp from '@backstage/cli-module-create-github-app'; +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'; @@ -34,7 +34,7 @@ export default [ auth, build, config, - createGithubApp, + github, info, lint, maintenance, diff --git a/packages/cli-internal/src/InternalCommandNode.ts b/packages/cli-internal/src/InternalCommandNode.ts index bfa1f90baa..233e9128cb 100644 --- a/packages/cli-internal/src/InternalCommandNode.ts +++ b/packages/cli-internal/src/InternalCommandNode.ts @@ -53,3 +53,17 @@ export const OpaqueCommandLeafNode = OpaqueType.create<{ 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 index 68b5affe64..0e69d45032 100644 --- a/packages/cli-internal/src/index.ts +++ b/packages/cli-internal/src/index.ts @@ -23,4 +23,5 @@ export type { export { OpaqueCommandTreeNode, OpaqueCommandLeafNode, + isCommandNodeHidden, } from './InternalCommandNode'; diff --git a/packages/cli-module-auth/bin/backstage-cli-module-auth b/packages/cli-module-auth/bin/backstage-cli-module-auth index 533974f6a9..885c06db9f 100755 --- a/packages/cli-module-auth/bin/backstage-cli-module-auth +++ b/packages/cli-module-auth/bin/backstage-cli-module-auth @@ -28,6 +28,6 @@ if (!isLocal) { const pkg = require('../package.json'); runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); } else { - require('@backstage/cli/config/nodeTransform.cjs'); + require('@backstage/cli-node/config/nodeTransform.cjs'); require('../src/cli'); } diff --git a/packages/cli-module-build/bin/backstage-cli-module-build b/packages/cli-module-build/bin/backstage-cli-module-build index 533974f6a9..885c06db9f 100755 --- a/packages/cli-module-build/bin/backstage-cli-module-build +++ b/packages/cli-module-build/bin/backstage-cli-module-build @@ -28,6 +28,6 @@ if (!isLocal) { const pkg = require('../package.json'); runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); } else { - require('@backstage/cli/config/nodeTransform.cjs'); + require('@backstage/cli-node/config/nodeTransform.cjs'); require('../src/cli'); } diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index 27f4fd3903..d13fa4bac0 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -72,7 +72,6 @@ "node-stdlib-browser": "^1.3.1", "npm-packlist": "^5.0.0", "p-queue": "^6.6.2", - "pirates": "^4.0.6", "postcss": "^8.1.0", "postcss-import": "^16.1.0", "process": "^0.11.10", diff --git a/packages/cli-module-build/src/lib/runner/runBackend.ts b/packages/cli-module-build/src/lib/runner/runBackend.ts index 73c3fe08c7..86c249e8be 100644 --- a/packages/cli-module-build/src/lib/runner/runBackend.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.ts @@ -21,16 +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 { findOwnPaths, targetPaths } from '@backstage/cli-common'; +import { targetPaths } from '@backstage/cli-common'; import spawn from 'cross-spawn'; const loaderArgs = [ '--enable-source-maps', '--require', - /* eslint-disable-next-line no-restricted-syntax */ - findOwnPaths(__dirname).resolve('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 = { diff --git a/packages/cli-module-build/src/tests/transforms/transforms.test.ts b/packages/cli-module-build/src/tests/transforms/transforms.test.ts index 40f40fac2d..71ee03df76 100644 --- a/packages/cli-module-build/src/tests/transforms/transforms.test.ts +++ b/packages/cli-module-build/src/tests/transforms/transforms.test.ts @@ -16,7 +16,6 @@ import { execFileSync } from 'node:child_process'; import { resolve as resolvePath } from 'node:path'; -import { findOwnPaths } from '@backstage/cli-common'; import { Output, buildPackage } from '../../lib/builder'; const exportValues = { @@ -56,8 +55,7 @@ function loadFixture(fixture: string) { 'node', [ '--import', - /* eslint-disable-next-line no-restricted-syntax */ - findOwnPaths(__dirname).resolve('config/nodeTransform.cjs'), + require.resolve('@backstage/cli-node/config/nodeTransform.cjs'), resolvePath(__dirname, `__fixtures__/${fixture}`), ], { encoding: 'utf8' }, diff --git a/packages/cli-module-config/bin/backstage-cli-module-config b/packages/cli-module-config/bin/backstage-cli-module-config index 533974f6a9..885c06db9f 100755 --- a/packages/cli-module-config/bin/backstage-cli-module-config +++ b/packages/cli-module-config/bin/backstage-cli-module-config @@ -28,6 +28,6 @@ if (!isLocal) { const pkg = require('../package.json'); runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); } else { - require('@backstage/cli/config/nodeTransform.cjs'); + require('@backstage/cli-node/config/nodeTransform.cjs'); require('../src/cli'); } diff --git a/packages/cli-module-create-github-app/cli-report.md b/packages/cli-module-create-github-app/cli-report.md deleted file mode 100644 index 7a7f7bb3a2..0000000000 --- a/packages/cli-module-create-github-app/cli-report.md +++ /dev/null @@ -1,26 +0,0 @@ -## CLI Report file for "@backstage/cli-module-create-github-app" - -> Do not edit this file. It is a report generated by `yarn build:api-reports` - -### `backstage-cli-module-create-github-app` - -``` -Usage: @backstage/cli-module-create-github-app [options] [command] - -Options: - -V, --version - -h, --help - -Commands: - create-github-app - help [command] -``` - -### `backstage-cli-module-create-github-app create-github-app` - -``` -Usage: @backstage/cli-module-create-github-app create-github-app - -Options: - -h, --help -``` diff --git a/packages/cli-module-create-github-app/.eslintrc.js b/packages/cli-module-github/.eslintrc.js similarity index 100% rename from packages/cli-module-create-github-app/.eslintrc.js rename to packages/cli-module-github/.eslintrc.js diff --git a/packages/cli-module-create-github-app/README.md b/packages/cli-module-github/README.md similarity index 92% rename from packages/cli-module-create-github-app/README.md rename to packages/cli-module-github/README.md index b10272ab02..6c38376901 100644 --- a/packages/cli-module-create-github-app/README.md +++ b/packages/cli-module-github/README.md @@ -1,4 +1,4 @@ -# @backstage/cli-module-create-github-app +# @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. diff --git a/packages/cli-module-create-github-app/bin/backstage-cli-module-create-github-app b/packages/cli-module-github/bin/backstage-cli-module-github similarity index 94% rename from packages/cli-module-create-github-app/bin/backstage-cli-module-create-github-app rename to packages/cli-module-github/bin/backstage-cli-module-github index 533974f6a9..885c06db9f 100755 --- a/packages/cli-module-create-github-app/bin/backstage-cli-module-create-github-app +++ b/packages/cli-module-github/bin/backstage-cli-module-github @@ -28,6 +28,6 @@ if (!isLocal) { const pkg = require('../package.json'); runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); } else { - require('@backstage/cli/config/nodeTransform.cjs'); + require('@backstage/cli-node/config/nodeTransform.cjs'); require('../src/cli'); } diff --git a/packages/cli-module-create-github-app/catalog-info.yaml b/packages/cli-module-github/catalog-info.yaml similarity index 66% rename from packages/cli-module-create-github-app/catalog-info.yaml rename to packages/cli-module-github/catalog-info.yaml index 66e59c5a27..b694da6f17 100644 --- a/packages/cli-module-create-github-app/catalog-info.yaml +++ b/packages/cli-module-github/catalog-info.yaml @@ -1,8 +1,8 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: backstage-cli-module-create-github-app - title: '@backstage/cli-module-create-github-app' + name: backstage-cli-module-github + title: '@backstage/cli-module-github' description: CLI module for Backstage CLI spec: lifecycle: experimental 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-create-github-app/package.json b/packages/cli-module-github/package.json similarity index 88% rename from packages/cli-module-create-github-app/package.json rename to packages/cli-module-github/package.json index 2196abc62e..1e8ca4fe6a 100644 --- a/packages/cli-module-create-github-app/package.json +++ b/packages/cli-module-github/package.json @@ -1,5 +1,5 @@ { - "name": "@backstage/cli-module-create-github-app", + "name": "@backstage/cli-module-github", "version": "0.0.0", "description": "CLI module for Backstage CLI", "backstage": { @@ -14,7 +14,7 @@ "repository": { "type": "git", "url": "https://github.com/backstage/backstage", - "directory": "packages/cli-module-create-github-app" + "directory": "packages/cli-module-github" }, "license": "Apache-2.0", "main": "src/index.ts", @@ -48,5 +48,5 @@ "@types/express": "^4.17.6", "@types/fs-extra": "^11.0.0" }, - "bin": "bin/backstage-cli-module-create-github-app" + "bin": "bin/backstage-cli-module-github" } diff --git a/packages/cli-module-create-github-app/report.api.md b/packages/cli-module-github/report.api.md similarity index 81% rename from packages/cli-module-create-github-app/report.api.md rename to packages/cli-module-github/report.api.md index 16ff642c69..c389a88b5f 100644 --- a/packages/cli-module-create-github-app/report.api.md +++ b/packages/cli-module-github/report.api.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/cli-module-create-github-app" +## 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/). diff --git a/packages/cli-module-create-github-app/src/cli.ts b/packages/cli-module-github/src/cli.ts similarity index 100% rename from packages/cli-module-create-github-app/src/cli.ts rename to packages/cli-module-github/src/cli.ts diff --git a/packages/cli-module-create-github-app/src/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-module-create-github-app/src/commands/create-github-app/GithubCreateAppServer.ts rename to packages/cli-module-github/src/commands/create-github-app/GithubCreateAppServer.ts diff --git a/packages/cli-module-create-github-app/src/commands/create-github-app/index.ts b/packages/cli-module-github/src/commands/create-github-app/index.ts similarity index 100% rename from packages/cli-module-create-github-app/src/commands/create-github-app/index.ts rename to packages/cli-module-github/src/commands/create-github-app/index.ts diff --git a/packages/cli-module-create-github-app/src/index.ts b/packages/cli-module-github/src/index.ts similarity index 100% rename from packages/cli-module-create-github-app/src/index.ts rename to packages/cli-module-github/src/index.ts diff --git a/packages/cli-module-info/bin/backstage-cli-module-info b/packages/cli-module-info/bin/backstage-cli-module-info index 533974f6a9..885c06db9f 100755 --- a/packages/cli-module-info/bin/backstage-cli-module-info +++ b/packages/cli-module-info/bin/backstage-cli-module-info @@ -28,6 +28,6 @@ if (!isLocal) { const pkg = require('../package.json'); runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); } else { - require('@backstage/cli/config/nodeTransform.cjs'); + require('@backstage/cli-node/config/nodeTransform.cjs'); require('../src/cli'); } diff --git a/packages/cli-module-lint/bin/backstage-cli-module-lint b/packages/cli-module-lint/bin/backstage-cli-module-lint index 533974f6a9..885c06db9f 100755 --- a/packages/cli-module-lint/bin/backstage-cli-module-lint +++ b/packages/cli-module-lint/bin/backstage-cli-module-lint @@ -28,6 +28,6 @@ if (!isLocal) { const pkg = require('../package.json'); runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); } else { - require('@backstage/cli/config/nodeTransform.cjs'); + require('@backstage/cli-node/config/nodeTransform.cjs'); require('../src/cli'); } diff --git a/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance b/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance index 533974f6a9..885c06db9f 100755 --- a/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance +++ b/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance @@ -28,6 +28,6 @@ if (!isLocal) { const pkg = require('../package.json'); runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); } else { - require('@backstage/cli/config/nodeTransform.cjs'); + require('@backstage/cli-node/config/nodeTransform.cjs'); require('../src/cli'); } diff --git a/packages/cli-module-migrate/bin/backstage-cli-module-migrate b/packages/cli-module-migrate/bin/backstage-cli-module-migrate index 533974f6a9..885c06db9f 100755 --- a/packages/cli-module-migrate/bin/backstage-cli-module-migrate +++ b/packages/cli-module-migrate/bin/backstage-cli-module-migrate @@ -28,6 +28,6 @@ if (!isLocal) { const pkg = require('../package.json'); runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); } else { - require('@backstage/cli/config/nodeTransform.cjs'); + require('@backstage/cli-node/config/nodeTransform.cjs'); require('../src/cli'); } diff --git a/packages/cli-module-new/bin/backstage-cli-module-new b/packages/cli-module-new/bin/backstage-cli-module-new index 533974f6a9..885c06db9f 100755 --- a/packages/cli-module-new/bin/backstage-cli-module-new +++ b/packages/cli-module-new/bin/backstage-cli-module-new @@ -28,6 +28,6 @@ if (!isLocal) { const pkg = require('../package.json'); runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); } else { - require('@backstage/cli/config/nodeTransform.cjs'); + require('@backstage/cli-node/config/nodeTransform.cjs'); require('../src/cli'); } 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 index 533974f6a9..885c06db9f 100755 --- a/packages/cli-module-test-jest/bin/backstage-cli-module-test-jest +++ b/packages/cli-module-test-jest/bin/backstage-cli-module-test-jest @@ -28,6 +28,6 @@ if (!isLocal) { const pkg = require('../package.json'); runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); } else { - require('@backstage/cli/config/nodeTransform.cjs'); + require('@backstage/cli-node/config/nodeTransform.cjs'); require('../src/cli'); } diff --git a/packages/cli-module-translations/bin/backstage-cli-module-translations b/packages/cli-module-translations/bin/backstage-cli-module-translations index 533974f6a9..885c06db9f 100755 --- a/packages/cli-module-translations/bin/backstage-cli-module-translations +++ b/packages/cli-module-translations/bin/backstage-cli-module-translations @@ -28,6 +28,6 @@ if (!isLocal) { const pkg = require('../package.json'); runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); } else { - require('@backstage/cli/config/nodeTransform.cjs'); + require('@backstage/cli-node/config/nodeTransform.cjs'); require('../src/cli'); } 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-module-build/config/nodeTransform.cjs b/packages/cli-node/config/nodeTransform.cjs similarity index 100% rename from packages/cli-module-build/config/nodeTransform.cjs rename to packages/cli-node/config/nodeTransform.cjs diff --git a/packages/cli-module-build/config/nodeTransformHooks.mjs b/packages/cli-node/config/nodeTransformHooks.mjs similarity index 100% rename from packages/cli-module-build/config/nodeTransformHooks.mjs rename to packages/cli-node/config/nodeTransformHooks.mjs diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index 95322ad2f0..e266088176 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "config" ], "scripts": { "build": "backstage-cli package build", @@ -35,11 +36,13 @@ "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", + "@swc/core": "^1.15.6", "@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" diff --git a/packages/cli-node/src/cli-module/createCliModule.ts b/packages/cli-node/src/cli-module/createCliModule.ts index 7d52e9bf43..b2facc3a67 100644 --- a/packages/cli-node/src/cli-module/createCliModule.ts +++ b/packages/cli-node/src/cli-module/createCliModule.ts @@ -62,8 +62,8 @@ export function createCliModule(options: { } const commands: CliCommand[] = []; - const commandsPromise = options - .init({ addCommand: command => commands.push(command) }) + const commandsPromise = Promise.resolve() + .then(() => options.init({ addCommand: command => commands.push(command) })) .then(() => commands); return OpaqueCliModule.createInstance('v1', { diff --git a/packages/cli-node/src/cli-module/runCliModule.ts b/packages/cli-node/src/cli-module/runCliModule.ts index 84893f0cac..0dc40e4c33 100644 --- a/packages/cli-node/src/cli-module/runCliModule.ts +++ b/packages/cli-node/src/cli-module/runCliModule.ts @@ -18,6 +18,7 @@ import { OpaqueCliModule, OpaqueCommandTreeNode, OpaqueCommandLeafNode, + isCommandNodeHidden, } from '@internal/cli'; import type { CommandNode } from '@internal/cli'; import { Command } from 'commander'; @@ -25,15 +26,6 @@ import chalk from 'chalk'; import { isError, stringifyError } from '@backstage/errors'; import type { CliModule, CliCommand } from './types'; -function isCommandHidden(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 => isCommandHidden(child)); -} - function buildCommandGraph(commands: ReadonlyArray): CommandNode[] { const graph: CommandNode[] = []; @@ -70,13 +62,11 @@ function buildCommandGraph(commands: ReadonlyArray): CommandNode[] { } function exitWithError(error: unknown): never { - if (!isError(error)) { - process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); - process.exit(1); - } process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); process.exit( - 'code' in error && typeof error.code === 'number' ? error.code : 1, + isError(error) && 'code' in error && typeof error.code === 'number' + ? error.code + : 1, ); } @@ -94,7 +84,7 @@ function registerCommands( const internal = OpaqueCommandTreeNode.toInternal(node); const treeParser = argParser .command(`${internal.name} [command]`, { - hidden: isCommandHidden(node), + hidden: isCommandNodeHidden(node), }) .description(internal.name); diff --git a/packages/cli/config/nodeTransform.cjs b/packages/cli/config/nodeTransform.cjs index 83815ebea9..c6866b2964 100644 --- a/packages/cli/config/nodeTransform.cjs +++ b/packages/cli/config/nodeTransform.cjs @@ -15,11 +15,11 @@ */ try { - require('@backstage/cli-module-build/config/nodeTransform.cjs'); + require('@backstage/cli-node/config/nodeTransform.cjs'); } catch (e) { if (e.code === 'MODULE_NOT_FOUND') { throw new Error( - '@backstage/cli-module-build is required to use the node transform. ' + + '@backstage/cli-node is required to use the node transform. ' + 'Please install it as a dependency.', ); } diff --git a/packages/cli/config/nodeTransformHooks.mjs b/packages/cli/config/nodeTransformHooks.mjs index 9fe9b327b5..274e24ed39 100644 --- a/packages/cli/config/nodeTransformHooks.mjs +++ b/packages/cli/config/nodeTransformHooks.mjs @@ -17,4 +17,4 @@ export { resolve, load, -} from '@backstage/cli-module-build/config/nodeTransformHooks.mjs'; +} from '@backstage/cli-node/config/nodeTransformHooks.mjs'; diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index e2c46cf99b..adbdfbe517 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -19,8 +19,8 @@ import { OpaqueCliModule, OpaqueCommandTreeNode, OpaqueCommandLeafNode, + isCommandNodeHidden, } from '@internal/cli'; -import type { CommandNode } from '@internal/cli'; import type { CliModule } from '@backstage/cli-node'; import { Command } from 'commander'; import { version } from './version'; @@ -29,15 +29,6 @@ import { exitWithError } from './errors'; import { ForwardedError } from '@backstage/errors'; import { isPromise } from 'node:util/types'; -function isNodeHidden(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 => isNodeHidden(child)); -} - type UninitializedFeature = | CliModule | CliModule[] @@ -45,6 +36,12 @@ type UninitializedFeature = 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; } @@ -136,7 +133,7 @@ export class CliInitializer { const internal = OpaqueCommandTreeNode.toInternal(node); const treeParser = argParser .command(`${internal.name} [command]`, { - hidden: isNodeHidden(node), + hidden: isCommandNodeHidden(node), }) .description(internal.name); diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index ff2f3eb4d6..6416681b27 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -51,17 +51,6 @@ jest.mock('./versions', () => ({ root: '1.2.3', '@backstage/cli': '1.0.0', '@backstage/cli-defaults': '1.0.0', - '@backstage/cli-module-auth': '1.0.0', - '@backstage/cli-module-build': '1.0.0', - '@backstage/cli-module-config': '1.0.0', - '@backstage/cli-module-create-github-app': '1.0.0', - '@backstage/cli-module-info': '1.0.0', - '@backstage/cli-module-lint': '1.0.0', - '@backstage/cli-module-maintenance': '1.0.0', - '@backstage/cli-module-migrate': '1.0.0', - '@backstage/cli-module-new': '1.0.0', - '@backstage/cli-module-test-jest': '1.0.0', - '@backstage/cli-module-translations': '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 e770b88f3f..006dd6d7b8 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -39,17 +39,6 @@ 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 cliModuleAuth } from '../../../cli-module-auth/package.json'; -import { version as cliModuleBuild } from '../../../cli-module-build/package.json'; -import { version as cliModuleConfig } from '../../../cli-module-config/package.json'; -import { version as cliModuleCreateGithubApp } from '../../../cli-module-create-github-app/package.json'; -import { version as cliModuleInfo } from '../../../cli-module-info/package.json'; -import { version as cliModuleLint } from '../../../cli-module-lint/package.json'; -import { version as cliModuleMaintenance } from '../../../cli-module-maintenance/package.json'; -import { version as cliModuleMigrate } from '../../../cli-module-migrate/package.json'; -import { version as cliModuleNew } from '../../../cli-module-new/package.json'; -import { version as cliModuleTestJest } from '../../../cli-module-test-jest/package.json'; -import { version as cliModuleTranslations } from '../../../cli-module-translations/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'; @@ -120,17 +109,6 @@ export const packageVersions = { '@backstage/catalog-model': catalogModel, '@backstage/cli': cli, '@backstage/cli-defaults': cliDefaults, - '@backstage/cli-module-auth': cliModuleAuth, - '@backstage/cli-module-build': cliModuleBuild, - '@backstage/cli-module-config': cliModuleConfig, - '@backstage/cli-module-create-github-app': cliModuleCreateGithubApp, - '@backstage/cli-module-info': cliModuleInfo, - '@backstage/cli-module-lint': cliModuleLint, - '@backstage/cli-module-maintenance': cliModuleMaintenance, - '@backstage/cli-module-migrate': cliModuleMigrate, - '@backstage/cli-module-new': cliModuleNew, - '@backstage/cli-module-test-jest': cliModuleTestJest, - '@backstage/cli-module-translations': cliModuleTranslations, '@backstage/config': config, '@backstage/core-app-api': coreAppApi, '@backstage/core-compat-api': coreCompatApi, diff --git a/packages/create-app/templates/next-app/package.json.hbs b/packages/create-app/templates/next-app/package.json.hbs index 110ebc160c..49436f5fcf 100644 --- a/packages/create-app/templates/next-app/package.json.hbs +++ b/packages/create-app/templates/next-app/package.json.hbs @@ -50,17 +50,7 @@ ], "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", - "@backstage/cli-module-auth": "^{{version '@backstage/cli-module-auth'}}", - "@backstage/cli-module-build": "^{{version '@backstage/cli-module-build'}}", - "@backstage/cli-module-config": "^{{version '@backstage/cli-module-config'}}", - "@backstage/cli-module-create-github-app": "^{{version '@backstage/cli-module-create-github-app'}}", - "@backstage/cli-module-info": "^{{version '@backstage/cli-module-info'}}", - "@backstage/cli-module-lint": "^{{version '@backstage/cli-module-lint'}}", - "@backstage/cli-module-maintenance": "^{{version '@backstage/cli-module-maintenance'}}", - "@backstage/cli-module-migrate": "^{{version '@backstage/cli-module-migrate'}}", - "@backstage/cli-module-new": "^{{version '@backstage/cli-module-new'}}", - "@backstage/cli-module-test-jest": "^{{version '@backstage/cli-module-test-jest'}}", - "@backstage/cli-module-translations": "^{{version '@backstage/cli-module-translations'}}", + "@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/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts b/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts index 7dfbba51ec..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 @@ -132,6 +132,11 @@ export async function runCliExtraction({ const pkgJson = await fs.readJson(resolvePath(fullDir, 'package.json')); if (!pkgJson.bin) { + if (pkgJson.backstage?.role === 'cli') { + throw new Error( + `CLI package ${pkgJson.name} is missing a "bin" field in its package.json`, + ); + } continue; } diff --git a/yarn.lock b/yarn.lock index 1c4ba53f5e..ff47745d26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2801,7 +2801,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-defaults@workspace:^, @backstage/cli-defaults@workspace:packages/cli-defaults": +"@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: @@ -2809,7 +2809,7 @@ __metadata: "@backstage/cli-module-auth": "workspace:^" "@backstage/cli-module-build": "workspace:^" "@backstage/cli-module-config": "workspace:^" - "@backstage/cli-module-create-github-app": "workspace:^" + "@backstage/cli-module-github": "workspace:^" "@backstage/cli-module-info": "workspace:^" "@backstage/cli-module-lint": "workspace:^" "@backstage/cli-module-maintenance": "workspace:^" @@ -2820,7 +2820,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-auth@workspace:*, @backstage/cli-module-auth@workspace:^, @backstage/cli-module-auth@workspace:packages/cli-module-auth": +"@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: @@ -2847,7 +2847,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-build@workspace:*, @backstage/cli-module-build@workspace:^, @backstage/cli-module-build@workspace:packages/cli-module-build": +"@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: @@ -2894,7 +2894,6 @@ __metadata: node-stdlib-browser: "npm:^1.3.1" npm-packlist: "npm:^5.0.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" @@ -2922,7 +2921,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-config@workspace:*, @backstage/cli-module-config@workspace:^, @backstage/cli-module-config@workspace:packages/cli-module-config": +"@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: @@ -2944,9 +2943,9 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-create-github-app@workspace:*, @backstage/cli-module-create-github-app@workspace:^, @backstage/cli-module-create-github-app@workspace:packages/cli-module-create-github-app": +"@backstage/cli-module-github@workspace:^, @backstage/cli-module-github@workspace:packages/cli-module-github": version: 0.0.0-use.local - resolution: "@backstage/cli-module-create-github-app@workspace:packages/cli-module-create-github-app" + resolution: "@backstage/cli-module-github@workspace:packages/cli-module-github" dependencies: "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" @@ -2962,11 +2961,11 @@ __metadata: react-dev-utils: "npm:^12.0.0-next.60" yaml: "npm:^2.0.0" bin: - cli-module-create-github-app: bin/backstage-cli-module-create-github-app + cli-module-github: bin/backstage-cli-module-github languageName: unknown linkType: soft -"@backstage/cli-module-info@workspace:*, @backstage/cli-module-info@workspace:^, @backstage/cli-module-info@workspace:packages/cli-module-info": +"@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: @@ -2982,7 +2981,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-lint@workspace:*, @backstage/cli-module-lint@workspace:^, @backstage/cli-module-lint@workspace:packages/cli-module-lint": +"@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: @@ -3003,7 +3002,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-maintenance@workspace:*, @backstage/cli-module-maintenance@workspace:^, @backstage/cli-module-maintenance@workspace:packages/cli-module-maintenance": +"@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: @@ -3020,7 +3019,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-migrate@workspace:*, @backstage/cli-module-migrate@workspace:^, @backstage/cli-module-migrate@workspace:packages/cli-module-migrate": +"@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: @@ -3047,7 +3046,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-new@workspace:*, @backstage/cli-module-new@workspace:^, @backstage/cli-module-new@workspace:packages/cli-module-new": +"@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: @@ -3079,7 +3078,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-test-jest@workspace:*, @backstage/cli-module-test-jest@workspace:^, @backstage/cli-module-test-jest@workspace:packages/cli-module-test-jest": +"@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: @@ -3112,7 +3111,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-module-translations@workspace:*, @backstage/cli-module-translations@workspace:^, @backstage/cli-module-translations@workspace:packages/cli-module-translations": +"@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: @@ -3139,12 +3138,14 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@manypkg/get-packages": "npm:^1.1.3" + "@swc/core": "npm:^1.15.6" "@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" @@ -45365,17 +45366,7 @@ __metadata: resolution: "root@workspace:." dependencies: "@backstage/cli": "workspace:*" - "@backstage/cli-module-auth": "workspace:*" - "@backstage/cli-module-build": "workspace:*" - "@backstage/cli-module-config": "workspace:*" - "@backstage/cli-module-create-github-app": "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:*" + "@backstage/cli-defaults": "workspace:*" "@backstage/codemods": "workspace:*" "@backstage/create-app": "workspace:*" "@backstage/e2e-test-utils": "workspace:*" From 92b0f79d9ce65e9f2c8024dbefc71bcaffbf8b9b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 13:03:17 +0100 Subject: [PATCH 147/188] Make @swc/core an optional peer dependency of cli-node The node transform config is only loaded at runtime when running backend processes, so @swc/core should not be a hard dependency for all cli-node consumers. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-node/package.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index e266088176..483b0eccff 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -36,7 +36,6 @@ "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", - "@swc/core": "^1.15.6", "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "^3.0.0", "chalk": "^4.0.0", @@ -52,5 +51,13 @@ "@backstage/cli": "workspace:^", "@backstage/test-utils": "workspace:^", "@types/yarnpkg__lockfile": "^1.1.4" + }, + "peerDependencies": { + "@swc/core": "^1.15.6" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } } } From ede7195275e64dad8cfc6f8eeab63645b5f6a588 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 13:08:09 +0100 Subject: [PATCH 148/188] chore: update yarn.lock for cli-node peer dependency changes Signed-off-by: Patrik Oldsberg Made-with: Cursor --- yarn.lock | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index ff47745d26..90ac7fa1c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3138,7 +3138,6 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@manypkg/get-packages": "npm:^1.1.3" - "@swc/core": "npm:^1.15.6" "@types/yarnpkg__lockfile": "npm:^1.1.4" "@yarnpkg/lockfile": "npm:^1.1.0" "@yarnpkg/parsers": "npm:^3.0.0" @@ -3149,6 +3148,11 @@ __metadata: 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 From 08dbd83cd3c1443c55792748c6dfa4bf12ca1872 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 13:12:00 +0100 Subject: [PATCH 149/188] chore: remove redundant cli.ts files from CLI modules The bin scripts already had the full runCliModule logic for the production path. Inline the same pattern for local dev and drop the intermediate src/cli.ts files that just duplicated it. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../bin/backstage-cli-module-auth | 13 +++++----- packages/cli-module-auth/src/cli.ts | 26 ------------------- .../bin/backstage-cli-module-build | 13 +++++----- packages/cli-module-build/src/cli.ts | 26 ------------------- .../bin/backstage-cli-module-config | 13 +++++----- packages/cli-module-config/src/cli.ts | 26 ------------------- .../bin/backstage-cli-module-github | 13 +++++----- packages/cli-module-github/src/cli.ts | 26 ------------------- .../bin/backstage-cli-module-info | 13 +++++----- packages/cli-module-info/src/cli.ts | 26 ------------------- .../bin/backstage-cli-module-lint | 13 +++++----- packages/cli-module-lint/src/cli.ts | 26 ------------------- .../bin/backstage-cli-module-maintenance | 13 +++++----- packages/cli-module-maintenance/src/cli.ts | 26 ------------------- .../bin/backstage-cli-module-migrate | 13 +++++----- packages/cli-module-migrate/src/cli.ts | 26 ------------------- .../bin/backstage-cli-module-new | 13 +++++----- packages/cli-module-new/src/cli.ts | 26 ------------------- .../bin/backstage-cli-module-test-jest | 13 +++++----- packages/cli-module-test-jest/src/cli.ts | 26 ------------------- .../bin/backstage-cli-module-translations | 13 +++++----- packages/cli-module-translations/src/cli.ts | 26 ------------------- 22 files changed, 66 insertions(+), 363 deletions(-) delete mode 100644 packages/cli-module-auth/src/cli.ts delete mode 100644 packages/cli-module-build/src/cli.ts delete mode 100644 packages/cli-module-config/src/cli.ts delete mode 100644 packages/cli-module-github/src/cli.ts delete mode 100644 packages/cli-module-info/src/cli.ts delete mode 100644 packages/cli-module-lint/src/cli.ts delete mode 100644 packages/cli-module-maintenance/src/cli.ts delete mode 100644 packages/cli-module-migrate/src/cli.ts delete mode 100644 packages/cli-module-new/src/cli.ts delete mode 100644 packages/cli-module-test-jest/src/cli.ts delete mode 100644 packages/cli-module-translations/src/cli.ts diff --git a/packages/cli-module-auth/bin/backstage-cli-module-auth b/packages/cli-module-auth/bin/backstage-cli-module-auth index 885c06db9f..68bdc76a4a 100755 --- a/packages/cli-module-auth/bin/backstage-cli-module-auth +++ b/packages/cli-module-auth/bin/backstage-cli-module-auth @@ -22,12 +22,11 @@ const isLocal = require('node:fs').existsSync( path.resolve(__dirname, '../src'), ); -if (!isLocal) { - const { runCliModule } = require('@backstage/cli-node'); - const cliModule = require('..').default; - const pkg = require('../package.json'); - runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); -} else { +if (isLocal) { require('@backstage/cli-node/config/nodeTransform.cjs'); - require('../src/cli'); } + +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/src/cli.ts b/packages/cli-module-auth/src/cli.ts deleted file mode 100644 index 6d4b925cf1..0000000000 --- a/packages/cli-module-auth/src/cli.ts +++ /dev/null @@ -1,26 +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 { runCliModule } from '@backstage/cli-node'; -import cliModule from './index'; - -const pkg = require('../package.json') as { name: string; version: string }; - -runCliModule({ - module: cliModule, - name: pkg.name, - version: pkg.version, -}); diff --git a/packages/cli-module-build/bin/backstage-cli-module-build b/packages/cli-module-build/bin/backstage-cli-module-build index 885c06db9f..68bdc76a4a 100755 --- a/packages/cli-module-build/bin/backstage-cli-module-build +++ b/packages/cli-module-build/bin/backstage-cli-module-build @@ -22,12 +22,11 @@ const isLocal = require('node:fs').existsSync( path.resolve(__dirname, '../src'), ); -if (!isLocal) { - const { runCliModule } = require('@backstage/cli-node'); - const cliModule = require('..').default; - const pkg = require('../package.json'); - runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); -} else { +if (isLocal) { require('@backstage/cli-node/config/nodeTransform.cjs'); - require('../src/cli'); } + +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/src/cli.ts b/packages/cli-module-build/src/cli.ts deleted file mode 100644 index 6d4b925cf1..0000000000 --- a/packages/cli-module-build/src/cli.ts +++ /dev/null @@ -1,26 +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 { runCliModule } from '@backstage/cli-node'; -import cliModule from './index'; - -const pkg = require('../package.json') as { name: string; version: string }; - -runCliModule({ - module: cliModule, - name: pkg.name, - version: pkg.version, -}); diff --git a/packages/cli-module-config/bin/backstage-cli-module-config b/packages/cli-module-config/bin/backstage-cli-module-config index 885c06db9f..68bdc76a4a 100755 --- a/packages/cli-module-config/bin/backstage-cli-module-config +++ b/packages/cli-module-config/bin/backstage-cli-module-config @@ -22,12 +22,11 @@ const isLocal = require('node:fs').existsSync( path.resolve(__dirname, '../src'), ); -if (!isLocal) { - const { runCliModule } = require('@backstage/cli-node'); - const cliModule = require('..').default; - const pkg = require('../package.json'); - runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); -} else { +if (isLocal) { require('@backstage/cli-node/config/nodeTransform.cjs'); - require('../src/cli'); } + +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/src/cli.ts b/packages/cli-module-config/src/cli.ts deleted file mode 100644 index 6d4b925cf1..0000000000 --- a/packages/cli-module-config/src/cli.ts +++ /dev/null @@ -1,26 +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 { runCliModule } from '@backstage/cli-node'; -import cliModule from './index'; - -const pkg = require('../package.json') as { name: string; version: string }; - -runCliModule({ - module: cliModule, - name: pkg.name, - version: pkg.version, -}); diff --git a/packages/cli-module-github/bin/backstage-cli-module-github b/packages/cli-module-github/bin/backstage-cli-module-github index 885c06db9f..68bdc76a4a 100755 --- a/packages/cli-module-github/bin/backstage-cli-module-github +++ b/packages/cli-module-github/bin/backstage-cli-module-github @@ -22,12 +22,11 @@ const isLocal = require('node:fs').existsSync( path.resolve(__dirname, '../src'), ); -if (!isLocal) { - const { runCliModule } = require('@backstage/cli-node'); - const cliModule = require('..').default; - const pkg = require('../package.json'); - runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); -} else { +if (isLocal) { require('@backstage/cli-node/config/nodeTransform.cjs'); - require('../src/cli'); } + +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/src/cli.ts b/packages/cli-module-github/src/cli.ts deleted file mode 100644 index 6d4b925cf1..0000000000 --- a/packages/cli-module-github/src/cli.ts +++ /dev/null @@ -1,26 +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 { runCliModule } from '@backstage/cli-node'; -import cliModule from './index'; - -const pkg = require('../package.json') as { name: string; version: string }; - -runCliModule({ - module: cliModule, - name: pkg.name, - version: pkg.version, -}); diff --git a/packages/cli-module-info/bin/backstage-cli-module-info b/packages/cli-module-info/bin/backstage-cli-module-info index 885c06db9f..68bdc76a4a 100755 --- a/packages/cli-module-info/bin/backstage-cli-module-info +++ b/packages/cli-module-info/bin/backstage-cli-module-info @@ -22,12 +22,11 @@ const isLocal = require('node:fs').existsSync( path.resolve(__dirname, '../src'), ); -if (!isLocal) { - const { runCliModule } = require('@backstage/cli-node'); - const cliModule = require('..').default; - const pkg = require('../package.json'); - runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); -} else { +if (isLocal) { require('@backstage/cli-node/config/nodeTransform.cjs'); - require('../src/cli'); } + +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/src/cli.ts b/packages/cli-module-info/src/cli.ts deleted file mode 100644 index 6d4b925cf1..0000000000 --- a/packages/cli-module-info/src/cli.ts +++ /dev/null @@ -1,26 +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 { runCliModule } from '@backstage/cli-node'; -import cliModule from './index'; - -const pkg = require('../package.json') as { name: string; version: string }; - -runCliModule({ - module: cliModule, - name: pkg.name, - version: pkg.version, -}); diff --git a/packages/cli-module-lint/bin/backstage-cli-module-lint b/packages/cli-module-lint/bin/backstage-cli-module-lint index 885c06db9f..68bdc76a4a 100755 --- a/packages/cli-module-lint/bin/backstage-cli-module-lint +++ b/packages/cli-module-lint/bin/backstage-cli-module-lint @@ -22,12 +22,11 @@ const isLocal = require('node:fs').existsSync( path.resolve(__dirname, '../src'), ); -if (!isLocal) { - const { runCliModule } = require('@backstage/cli-node'); - const cliModule = require('..').default; - const pkg = require('../package.json'); - runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); -} else { +if (isLocal) { require('@backstage/cli-node/config/nodeTransform.cjs'); - require('../src/cli'); } + +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/src/cli.ts b/packages/cli-module-lint/src/cli.ts deleted file mode 100644 index 6d4b925cf1..0000000000 --- a/packages/cli-module-lint/src/cli.ts +++ /dev/null @@ -1,26 +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 { runCliModule } from '@backstage/cli-node'; -import cliModule from './index'; - -const pkg = require('../package.json') as { name: string; version: string }; - -runCliModule({ - module: cliModule, - name: pkg.name, - version: pkg.version, -}); diff --git a/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance b/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance index 885c06db9f..68bdc76a4a 100755 --- a/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance +++ b/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance @@ -22,12 +22,11 @@ const isLocal = require('node:fs').existsSync( path.resolve(__dirname, '../src'), ); -if (!isLocal) { - const { runCliModule } = require('@backstage/cli-node'); - const cliModule = require('..').default; - const pkg = require('../package.json'); - runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); -} else { +if (isLocal) { require('@backstage/cli-node/config/nodeTransform.cjs'); - require('../src/cli'); } + +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/src/cli.ts b/packages/cli-module-maintenance/src/cli.ts deleted file mode 100644 index 6d4b925cf1..0000000000 --- a/packages/cli-module-maintenance/src/cli.ts +++ /dev/null @@ -1,26 +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 { runCliModule } from '@backstage/cli-node'; -import cliModule from './index'; - -const pkg = require('../package.json') as { name: string; version: string }; - -runCliModule({ - module: cliModule, - name: pkg.name, - version: pkg.version, -}); diff --git a/packages/cli-module-migrate/bin/backstage-cli-module-migrate b/packages/cli-module-migrate/bin/backstage-cli-module-migrate index 885c06db9f..68bdc76a4a 100755 --- a/packages/cli-module-migrate/bin/backstage-cli-module-migrate +++ b/packages/cli-module-migrate/bin/backstage-cli-module-migrate @@ -22,12 +22,11 @@ const isLocal = require('node:fs').existsSync( path.resolve(__dirname, '../src'), ); -if (!isLocal) { - const { runCliModule } = require('@backstage/cli-node'); - const cliModule = require('..').default; - const pkg = require('../package.json'); - runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); -} else { +if (isLocal) { require('@backstage/cli-node/config/nodeTransform.cjs'); - require('../src/cli'); } + +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/src/cli.ts b/packages/cli-module-migrate/src/cli.ts deleted file mode 100644 index 6d4b925cf1..0000000000 --- a/packages/cli-module-migrate/src/cli.ts +++ /dev/null @@ -1,26 +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 { runCliModule } from '@backstage/cli-node'; -import cliModule from './index'; - -const pkg = require('../package.json') as { name: string; version: string }; - -runCliModule({ - module: cliModule, - name: pkg.name, - version: pkg.version, -}); diff --git a/packages/cli-module-new/bin/backstage-cli-module-new b/packages/cli-module-new/bin/backstage-cli-module-new index 885c06db9f..68bdc76a4a 100755 --- a/packages/cli-module-new/bin/backstage-cli-module-new +++ b/packages/cli-module-new/bin/backstage-cli-module-new @@ -22,12 +22,11 @@ const isLocal = require('node:fs').existsSync( path.resolve(__dirname, '../src'), ); -if (!isLocal) { - const { runCliModule } = require('@backstage/cli-node'); - const cliModule = require('..').default; - const pkg = require('../package.json'); - runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); -} else { +if (isLocal) { require('@backstage/cli-node/config/nodeTransform.cjs'); - require('../src/cli'); } + +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/src/cli.ts b/packages/cli-module-new/src/cli.ts deleted file mode 100644 index 6d4b925cf1..0000000000 --- a/packages/cli-module-new/src/cli.ts +++ /dev/null @@ -1,26 +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 { runCliModule } from '@backstage/cli-node'; -import cliModule from './index'; - -const pkg = require('../package.json') as { name: string; version: string }; - -runCliModule({ - module: cliModule, - name: pkg.name, - version: pkg.version, -}); 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 index 885c06db9f..68bdc76a4a 100755 --- a/packages/cli-module-test-jest/bin/backstage-cli-module-test-jest +++ b/packages/cli-module-test-jest/bin/backstage-cli-module-test-jest @@ -22,12 +22,11 @@ const isLocal = require('node:fs').existsSync( path.resolve(__dirname, '../src'), ); -if (!isLocal) { - const { runCliModule } = require('@backstage/cli-node'); - const cliModule = require('..').default; - const pkg = require('../package.json'); - runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); -} else { +if (isLocal) { require('@backstage/cli-node/config/nodeTransform.cjs'); - require('../src/cli'); } + +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/src/cli.ts b/packages/cli-module-test-jest/src/cli.ts deleted file mode 100644 index 6d4b925cf1..0000000000 --- a/packages/cli-module-test-jest/src/cli.ts +++ /dev/null @@ -1,26 +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 { runCliModule } from '@backstage/cli-node'; -import cliModule from './index'; - -const pkg = require('../package.json') as { name: string; version: string }; - -runCliModule({ - module: cliModule, - name: pkg.name, - version: pkg.version, -}); diff --git a/packages/cli-module-translations/bin/backstage-cli-module-translations b/packages/cli-module-translations/bin/backstage-cli-module-translations index 885c06db9f..68bdc76a4a 100755 --- a/packages/cli-module-translations/bin/backstage-cli-module-translations +++ b/packages/cli-module-translations/bin/backstage-cli-module-translations @@ -22,12 +22,11 @@ const isLocal = require('node:fs').existsSync( path.resolve(__dirname, '../src'), ); -if (!isLocal) { - const { runCliModule } = require('@backstage/cli-node'); - const cliModule = require('..').default; - const pkg = require('../package.json'); - runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); -} else { +if (isLocal) { require('@backstage/cli-node/config/nodeTransform.cjs'); - require('../src/cli'); } + +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/src/cli.ts b/packages/cli-module-translations/src/cli.ts deleted file mode 100644 index 6d4b925cf1..0000000000 --- a/packages/cli-module-translations/src/cli.ts +++ /dev/null @@ -1,26 +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 { runCliModule } from '@backstage/cli-node'; -import cliModule from './index'; - -const pkg = require('../package.json') as { name: string; version: string }; - -runCliModule({ - module: cliModule, - name: pkg.name, - version: pkg.version, -}); From 98b6b9701b6891b8c8d9e3ba9d498fdcfb5a5ba6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 13:20:01 +0100 Subject: [PATCH 150/188] Address review feedback on plugin README updates - Apply freben's suggested wording for old frontend system sections to clarify they can be skipped on the new frontend system - Fix scaffolder README: add missing imports (AnyApiFactory, scmIntegrationsApiRef), closing bracket, and update link text - Fix devtools README: correct closing tag from `` to `` - Fix kubernetes README: reword "must be explicitly added" to avoid conflicting with feature discovery paragraph Signed-off-by: Patrik Oldsberg Made-with: Cursor --- plugins/api-docs/README.md | 2 +- plugins/catalog-graph/README.md | 2 +- plugins/catalog/README.md | 2 +- plugins/devtools/README.md | 4 ++-- plugins/kubernetes/README.md | 2 +- plugins/org/README.md | 2 +- plugins/scaffolder/README.md | 9 ++++++--- plugins/user-settings/README.md | 2 +- 8 files changed, 14 insertions(+), 11 deletions(-) diff --git a/plugins/api-docs/README.md b/plugins/api-docs/README.md index 5650b378f5..f71b796ec4 100644 --- a/plugins/api-docs/README.md +++ b/plugins/api-docs/README.md @@ -342,7 +342,7 @@ 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. +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: diff --git a/plugins/catalog-graph/README.md b/plugins/catalog-graph/README.md index b1af843190..058fc28164 100644 --- a/plugins/catalog-graph/README.md +++ b/plugins/catalog-graph/README.md @@ -143,7 +143,7 @@ import { ## Old Frontend System -If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app. +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`: diff --git a/plugins/catalog/README.md b/plugins/catalog/README.md index ae8ddd1033..af8c82b35c 100644 --- a/plugins/catalog/README.md +++ b/plugins/catalog/README.md @@ -24,7 +24,7 @@ Once installed, the plugin is automatically available in your app through the de ## Old Frontend System -If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app. +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` diff --git a/plugins/devtools/README.md b/plugins/devtools/README.md index b049839861..3f9a3da747 100644 --- a/plugins/devtools/README.md +++ b/plugins/devtools/README.md @@ -202,7 +202,7 @@ Here's how to add the Catalog Unprocessed Entities tab: ## Old Frontend System -If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app. +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: @@ -211,7 +211,7 @@ If your Backstage app uses the old frontend system, you need to manually wire th 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: +3. In this same file just before the closing ``, this will be near the bottom of the file, add this line: ```ts } /> diff --git a/plugins/kubernetes/README.md b/plugins/kubernetes/README.md index baf0ab42a3..fe64aea257 100644 --- a/plugins/kubernetes/README.md +++ b/plugins/kubernetes/README.md @@ -14,7 +14,7 @@ See our announcement blog post [New Backstage feature: Kubernetes for Service Ow ## Setup & Configuration -This plugin must be explicitly added to a Backstage app, along with its peer backend plugin. +This plugin must be installed in a Backstage app, along with its peer backend plugin. ```bash # From your Backstage root directory diff --git a/plugins/org/README.md b/plugins/org/README.md index 9c8050e940..062f88fa98 100644 --- a/plugins/org/README.md +++ b/plugins/org/README.md @@ -43,7 +43,7 @@ For the full list of available extensions and their configuration options, see t ## Old Frontend System -If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app. +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 diff --git a/plugins/scaffolder/README.md b/plugins/scaffolder/README.md index 6a2ad697b5..bc3f0f8ed6 100644 --- a/plugins/scaffolder/README.md +++ b/plugins/scaffolder/README.md @@ -24,18 +24,20 @@ Once installed, the plugin is automatically available in your app through the de ### Troubleshooting -If you encounter the [issue of closing EventStream](https://github.com/backstage/backstage/issues/5535) +If you encounter [issues with early closure of the `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 { + AnyApiFactory, createApiFactory, discoveryApiRef, fetchApiRef, identityApiRef, } from '@backstage/core-plugin-api'; +import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { scaffolderApiRef, ScaffolderClient, @@ -60,6 +62,7 @@ export const apis: AnyApiFactory[] = [ }), }), // ... other factories +]; ``` This replaces the default implementation of the `scaffolderApiRef`. @@ -71,11 +74,11 @@ to launch the plugin locally using the `createDevApp` of the `./dev/index.tsx` f 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 ! +**NOTE:** Don't forget to open a second terminal and to launch the backend there, using `yarn start backend` and to specify the locations of the templates to play with! ## Old Frontend System -If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app. +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` diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index 2fa5e5db39..8644001fe1 100644 --- a/plugins/user-settings/README.md +++ b/plugins/user-settings/README.md @@ -22,7 +22,7 @@ Once installed, the plugin is automatically available in your app through the de ## Old Frontend System -If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app. +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 From 3a76037381d4448da16f01246107fd2b76f3c32b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:05:42 +0000 Subject: [PATCH 151/188] Bump undici from 5.29.0 to 7.24.4 Bumps [undici](https://github.com/nodejs/undici) from 5.29.0 to 7.24.4. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v5.29.0...v7.24.4) --- updated-dependencies: - dependency-name: undici dependency-version: 7.24.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 90ac7fa1c1..b533dd3afc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -49175,9 +49175,9 @@ __metadata: linkType: hard "undici@npm:^7.2.3, undici@npm:^7.22.0": - version: 7.24.1 - resolution: "undici@npm:7.24.1" - checksum: 10/0af4ec2fc2ae50b1d0636895793ea2274a89a7cd445690089a1b957c4ac4a0336e48ab9ac48a4e8f5023d4b5eab56368c3f17c75d6b4c81b4281b674d1e60c9f + version: 7.24.4 + resolution: "undici@npm:7.24.4" + checksum: 10/747e76e0fd685ae1bb6fc1a2ebce0caca4ee8bd5599a77da36a3f94eac146987a9547bdbec7a74d18c0776df8ad348dccb4209901ca83fc4076f560de0d5dc7a languageName: node linkType: hard From d9d2dd6827e2f614eec6acc4e2a36c75f78b96a7 Mon Sep 17 00:00:00 2001 From: Johan Persson Date: Fri, 13 Mar 2026 17:21:28 +0100 Subject: [PATCH 152/188] feat(ui): add SearchAutocomplete component Add SearchAutocomplete and SearchAutocompleteItem components for building accessible search-with-results patterns. Built on React Aria's Autocomplete with virtual focus for keyboard navigation and a non-modal popover for results. Features: - Controlled input via inputValue/onInputChange - Configurable popover width and placement - Rich content support per result item - Item selection via onAction - defaultOpen prop for visual testing - Close on interact outside and input clear Includes Storybook stories, docs-ui documentation, changeset, and API report. Signed-off-by: Johan Persson --- .changeset/fine-dragons-scream.md | 7 + .../search-autocomplete/components.tsx | 156 ++++++ .../components/search-autocomplete/page.mdx | 95 ++++ .../search-autocomplete/props-definition.tsx | 97 ++++ .../search-autocomplete/snippets.ts | 139 ++++++ docs-ui/src/utils/data.ts | 4 + docs-ui/src/utils/types.ts | 1 + packages/ui/report.api.md | 79 ++++ .../SearchAutocomplete.module.css | 214 +++++++++ .../SearchAutocomplete.stories.tsx | 445 ++++++++++++++++++ .../SearchAutocomplete/SearchAutocomplete.tsx | 204 ++++++++ .../SearchAutocomplete/definition.ts | 71 +++ .../components/SearchAutocomplete/index.ts | 27 ++ .../components/SearchAutocomplete/types.ts | 101 ++++ packages/ui/src/definitions.ts | 4 + packages/ui/src/index.ts | 1 + 16 files changed, 1645 insertions(+) create mode 100644 .changeset/fine-dragons-scream.md create mode 100644 docs-ui/src/app/components/search-autocomplete/components.tsx create mode 100644 docs-ui/src/app/components/search-autocomplete/page.mdx create mode 100644 docs-ui/src/app/components/search-autocomplete/props-definition.tsx create mode 100644 docs-ui/src/app/components/search-autocomplete/snippets.ts create mode 100644 packages/ui/src/components/SearchAutocomplete/SearchAutocomplete.module.css create mode 100644 packages/ui/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx create mode 100644 packages/ui/src/components/SearchAutocomplete/SearchAutocomplete.tsx create mode 100644 packages/ui/src/components/SearchAutocomplete/definition.ts create mode 100644 packages/ui/src/components/SearchAutocomplete/index.ts create mode 100644 packages/ui/src/components/SearchAutocomplete/types.ts diff --git a/.changeset/fine-dragons-scream.md b/.changeset/fine-dragons-scream.md new file mode 100644 index 0000000000..b1373e25f3 --- /dev/null +++ b/.changeset/fine-dragons-scream.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Added `SearchAutocomplete` and `SearchAutocompleteItem` components for building accessible search-with-results patterns. Built on React Aria's Autocomplete with keyboard navigation and screen reader support. Designed for async/external search results with a configurable popover width. + +**Affected components:** SearchAutocomplete, SearchAutocompleteItem diff --git a/docs-ui/src/app/components/search-autocomplete/components.tsx b/docs-ui/src/app/components/search-autocomplete/components.tsx new file mode 100644 index 0000000000..4052ee0e3f --- /dev/null +++ b/docs-ui/src/app/components/search-autocomplete/components.tsx @@ -0,0 +1,156 @@ +'use client'; + +import { useState } from 'react'; +import { + SearchAutocomplete, + SearchAutocompleteItem, +} from '../../../../../packages/ui/src/components/SearchAutocomplete/SearchAutocomplete'; +import { PluginHeader } from '../../../../../packages/ui/src/components/PluginHeader/PluginHeader'; +import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex'; +import { Text } from '../../../../../packages/ui/src/components/Text/Text'; +import { MemoryRouter } from 'react-router-dom'; + +const fruits = [ + { id: 'apple', name: 'Apple', description: 'A round fruit' }, + { id: 'banana', name: 'Banana', description: 'A yellow curved fruit' }, + { id: 'blueberry', name: 'Blueberry', description: 'A small blue berry' }, + { id: 'cherry', name: 'Cherry', description: 'A small red stone fruit' }, + { id: 'grape', name: 'Grape', description: 'Grows in clusters on vines' }, + { id: 'lemon', name: 'Lemon', description: 'A sour yellow citrus fruit' }, + { id: 'mango', name: 'Mango', description: 'A tropical stone fruit' }, + { id: 'orange', name: 'Orange', description: 'A citrus fruit' }, + { id: 'peach', name: 'Peach', description: 'A fuzzy stone fruit' }, + { + id: 'strawberry', + name: 'Strawberry', + description: 'A red fruit with seeds on its surface', + }, +]; + +export const WithItems = () => { + 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/utils/data.ts b/docs-ui/src/utils/data.ts index 753473ab64..fc1f841b55 100644 --- a/docs-ui/src/utils/data.ts +++ b/docs-ui/src/utils/data.ts @@ -85,6 +85,10 @@ export const components: Page[] = [ title: 'RadioGroup', slug: 'radio-group', }, + { + title: 'SearchAutocomplete', + slug: 'search-autocomplete', + }, { title: 'SearchField', slug: 'search-field', diff --git a/docs-ui/src/utils/types.ts b/docs-ui/src/utils/types.ts index c2c383796f..670bee3f05 100644 --- a/docs-ui/src/utils/types.ts +++ b/docs-ui/src/utils/types.ts @@ -23,6 +23,7 @@ export type Component = | 'password-field' | 'radio-group' | 'scrollarea' + | 'search-autocomplete' | 'searchfield' | 'select' | 'skeleton' diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 09454cecc9..b254b4f773 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -2074,6 +2074,85 @@ export type RowRenderFn = (params: { 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 diff --git a/packages/ui/src/components/SearchAutocomplete/SearchAutocomplete.module.css b/packages/ui/src/components/SearchAutocomplete/SearchAutocomplete.module.css new file mode 100644 index 0000000000..ca6de73402 --- /dev/null +++ b/packages/ui/src/components/SearchAutocomplete/SearchAutocomplete.module.css @@ -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. + */ + +@layer tokens, base, components, utilities; + +@layer components { + .bui-SearchAutocomplete { + display: flex; + align-items: center; + border-radius: var(--bui-radius-2); + background-color: var(--bui-bg-neutral-1); + transition: box-shadow 0.2s ease-in-out; + font-family: var(--bui-font-regular); + + &:has([data-focused]) { + box-shadow: inset 0 0 0 1px var(--bui-ring); + } + + &[data-size='small'] { + --search-autocomplete-item-height: 2rem; + height: 2rem; + } + + &[data-size='medium'] { + --search-autocomplete-item-height: 2.5rem; + height: 2.5rem; + } + } + + .bui-SearchAutocompleteSearchField { + display: flex; + align-items: center; + flex: 1; + + & > div { + display: flex; + align-items: center; + flex: 1; + } + + & svg { + flex: 0 0 auto; + color: var(--bui-fg-primary); + margin-inline-start: var(--bui-space-2); + + .bui-SearchAutocomplete[data-size='small'] & { + width: 1rem; + height: 1rem; + } + + .bui-SearchAutocomplete[data-size='medium'] & { + width: 1.25rem; + height: 1.25rem; + } + } + } + + .bui-SearchAutocompleteInput { + flex: 1; + display: flex; + align-items: center; + padding: 0; + padding-inline: var(--bui-space-2); + border: none; + background-color: transparent; + font-size: var(--bui-font-size-3); + font-family: var(--bui-font-regular); + font-weight: var(--bui-font-weight-regular); + color: var(--bui-fg-primary); + width: 100%; + height: 100%; + outline: none; + + &::-webkit-search-cancel-button, + &::-webkit-search-decoration { + -webkit-appearance: none; + } + + &::placeholder { + color: var(--bui-fg-secondary); + } + } + + .bui-SearchAutocompleteClear { + flex: 0 0 auto; + display: grid; + place-content: center; + background-color: transparent; + border: none; + padding: 0; + margin: 0; + cursor: pointer; + color: var(--bui-fg-secondary); + transition: color 0.2s ease-in-out; + width: var(--search-autocomplete-item-height); + height: var(--search-autocomplete-item-height); + + &:hover { + color: var(--bui-fg-primary); + } + + & svg { + width: 1rem; + height: 1rem; + } + } + + .bui-SearchAutocompletePopover { + --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(--menu-border-radius); + background: var(--bui-bg-app); + color: var(--bui-fg-primary); + outline: none; + transition: transform 200ms, opacity 200ms; + min-height: 0; + overflow: hidden; + + &[data-entering], + &[data-exiting] { + transform: var(--origin); + opacity: 0; + } + + &[data-placement='top'] { + --origin: translateY(8px); + } + + &[data-placement='bottom'] { + --origin: translateY(-8px); + } + } + + .bui-SearchAutocompleteInner { + border-radius: var(--menu-border-radius); + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; + } + + .bui-SearchAutocompleteListBox { + max-height: inherit; + box-sizing: border-box; + overflow: auto; + min-width: 150px; + outline: none; + padding-block: var(--bui-space-1); + + &[data-stale] { + opacity: 0.6; + } + } + + .bui-SearchAutocompleteItem { + padding-inline: var(--bui-space-1); + display: block; + + &:focus-visible { + outline: none; + } + + &[data-focus-visible] { + outline: 2px solid var(--bui-ring); + outline-offset: -2px; + } + + &[data-focused] .bui-SearchAutocompleteItemContent, + &[data-hovered] .bui-SearchAutocompleteItemContent { + background: var(--bui-bg-neutral-2); + color: var(--bui-fg-primary); + } + } + + .bui-SearchAutocompleteItemContent { + display: flex; + flex-direction: column; + gap: var(--bui-space-1); + min-height: 2rem; + padding-inline: var(--bui-space-2); + padding-block: var(--bui-space-2); + border-radius: var(--bui-radius-2); + outline: none; + cursor: default; + color: var(--bui-fg-primary); + font-size: var(--bui-font-size-3); + } + + .bui-SearchAutocompleteLoadingState, + .bui-SearchAutocompleteEmptyState { + padding-inline: var(--bui-space-3); + padding-block: var(--bui-space-2); + color: var(--bui-fg-secondary); + font-size: var(--bui-font-size-3); + font-family: var(--bui-font-regular); + font-weight: var(--bui-font-weight-regular); + } +} diff --git a/packages/ui/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx b/packages/ui/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx new file mode 100644 index 0000000000..1a044a9da8 --- /dev/null +++ b/packages/ui/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx @@ -0,0 +1,445 @@ +/* + * 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-disable no-restricted-syntax */ + +import preview from '../../../../../.storybook/preview'; +import { useState, useEffect } from 'react'; +import { + SearchAutocomplete, + SearchAutocompleteItem, +} from './SearchAutocomplete'; +import { PluginHeader } from '../PluginHeader'; +import { Flex } from '../Flex'; +import { Text } from '../Text'; +import { ButtonIcon } from '../ButtonIcon'; +import { RiCactusLine } from '@remixicon/react'; +import { MemoryRouter } from 'react-router-dom'; + +const meta = preview.meta({ + title: 'Backstage UI/SearchAutocomplete', + component: SearchAutocomplete, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium'], + }, + placeholder: { + control: 'text', + }, + popoverWidth: { + control: 'text', + }, + }, +}); + +const fruits = [ + { id: 'apple', name: 'Apple', description: 'A round fruit' }, + { id: 'banana', name: 'Banana', description: 'A yellow curved fruit' }, + { id: 'blueberry', name: 'Blueberry', description: 'A small blue berry' }, + { id: 'cherry', name: 'Cherry', description: 'A small red stone fruit' }, + { id: 'grape', name: 'Grape', description: 'Grows in clusters on vines' }, + { id: 'lemon', name: 'Lemon', description: 'A sour yellow citrus fruit' }, + { id: 'mango', name: 'Mango', description: 'A tropical stone fruit' }, + { id: 'orange', name: 'Orange', description: 'A citrus fruit' }, + { id: 'peach', name: 'Peach', description: 'A fuzzy stone fruit' }, + { + id: 'strawberry', + name: 'Strawberry', + description: 'A red fruit with seeds on its surface', + }, +]; + +export const WithItems = meta.story({ + args: { + 'aria-label': 'Search fruits', + placeholder: 'Search fruits...', + style: { maxWidth: '300px' }, + }, + render: function Render(args) { + const [inputValue, setInputValue] = useState(''); + + const filtered = fruits.filter(fruit => + fruit.name.toLowerCase().includes(inputValue.toLowerCase()), + ); + + return ( + + {filtered.map(fruit => ( + + {fruit.name} + + ))} + + ); + }, +}); + +export const WithRichContent = meta.story({ + args: { + 'aria-label': 'Search fruits', + placeholder: 'Search fruits...', + style: { maxWidth: '300px' }, + }, + render: function Render(args) { + 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 WithAsyncItems = meta.story({ + args: { + 'aria-label': 'Search fruits', + placeholder: 'Search fruits...', + style: { maxWidth: '300px' }, + }, + render: function Render(args) { + const [inputValue, setInputValue] = useState(''); + const [items, setItems] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (!inputValue) { + setItems([]); + return undefined; + } + + setIsLoading(true); + const timeout = setTimeout(() => { + setItems( + fruits.filter(fruit => + fruit.name.toLowerCase().includes(inputValue.toLowerCase()), + ), + ); + setIsLoading(false); + }, 2000); + + return () => clearTimeout(timeout); + }, [inputValue]); + + return ( + + {items.map(item => ( + + {item.name} + + {item.description} + + + ))} + + ); + }, +}); + +export const WithSelection = meta.story({ + args: { + 'aria-label': 'Search fruits', + placeholder: 'Search fruits...', + style: { maxWidth: '300px' }, + }, + render: function Render(args) { + 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'} + + ); + }, +}); + +export const Sizes = meta.story({ + args: { + 'aria-label': 'Search', + placeholder: 'Search...', + }, + render: function Render(args) { + const [inputValue, setInputValue] = useState(''); + + const filtered = fruits.filter(fruit => + fruit.name.toLowerCase().includes(inputValue.toLowerCase()), + ); + + return ( + + + {filtered.map(fruit => ( + + {fruit.name} + + ))} + + + {filtered.map(fruit => ( + + {fruit.name} + + ))} + + + ); + }, +}); + +export const InHeader = meta.story({ + args: { + 'aria-label': 'Search', + placeholder: 'Search...', + }, + decorators: [ + Story => ( + + + + ), + ], + render: function Render(args) { + const [inputValue, setInputValue] = useState(''); + + const filtered = fruits.filter(fruit => + fruit.name.toLowerCase().includes(inputValue.toLowerCase()), + ); + + return ( + + } + size="small" + variant="secondary" + /> + + {filtered.map(fruit => ( + + {fruit.name} + + ))} + + } + size="small" + variant="secondary" + /> + + } + /> + ); + }, +}); + +export const WithPopoverWidth = meta.story({ + args: { + 'aria-label': 'Search fruits', + placeholder: 'Search fruits...', + popoverWidth: '500px', + style: { maxWidth: '300px' }, + }, + render: function Render(args) { + 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 OpenByDefault = meta.story({ + args: { + 'aria-label': 'Search fruits', + placeholder: 'Search fruits...', + defaultOpen: true, + style: { maxWidth: '300px' }, + }, + render: function Render(args) { + const [inputValue, setInputValue] = useState('ap'); + + const filtered = fruits.filter(fruit => + fruit.name.toLowerCase().includes(inputValue.toLowerCase()), + ); + + return ( + + {filtered.map(fruit => ( + + {fruit.name} + + {fruit.description} + + + ))} + + ); + }, +}); + +export const OpenWithLoading = meta.story({ + args: { + 'aria-label': 'Search fruits', + placeholder: 'Search fruits...', + defaultOpen: true, + isLoading: true, + inputValue: 'ap', + style: { maxWidth: '300px' }, + }, + render: function Render(args) { + return ( + {}}> + {fruits.slice(0, 3).map(fruit => ( + + {fruit.name} + + {fruit.description} + + + ))} + + ); + }, +}); diff --git a/packages/ui/src/components/SearchAutocomplete/SearchAutocomplete.tsx b/packages/ui/src/components/SearchAutocomplete/SearchAutocomplete.tsx new file mode 100644 index 0000000000..d8c4640e26 --- /dev/null +++ b/packages/ui/src/components/SearchAutocomplete/SearchAutocomplete.tsx @@ -0,0 +1,204 @@ +/* + * 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 { Children, useRef } from 'react'; +import { useInteractOutside } from '@react-aria/interactions'; +import { + Autocomplete, + SearchField as RASearchField, + Input, + Button as RAButton, + Popover as RAPopover, + ListBox, + ListBoxItem, + OverlayTriggerStateContext, +} from 'react-aria-components'; +import { useOverlayTriggerState } from '@react-stately/overlays'; +import { RiSearch2Line, RiCloseCircleLine } from '@remixicon/react'; +import { useDefinition } from '../../hooks/useDefinition'; +import { + SearchAutocompleteDefinition, + SearchAutocompleteItemDefinition, +} from './definition'; +import { Box } from '../Box'; +import { BgReset } from '../../hooks/useBg'; +import { VisuallyHidden } from '../VisuallyHidden'; + +import type { + SearchAutocompleteProps, + SearchAutocompleteItemProps, +} from './types'; + +const SearchAutocompleteEmptyState = () => { + const { ownProps } = useDefinition(SearchAutocompleteDefinition, {}); + return
No results found.
; +}; + +/** @public */ +export function SearchAutocomplete(props: SearchAutocompleteProps) { + const { ownProps, dataAttributes } = useDefinition( + SearchAutocompleteDefinition, + props, + ); + const { + classes, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, + inputValue, + onInputChange, + placeholder, + popoverWidth, + popoverPlacement = 'bottom start', + children, + isLoading, + defaultOpen, + style, + } = ownProps; + + const triggerRef = useRef(null); + const popoverRef = useRef(null); + const hasValue = !!inputValue; + const hasChildren = Children.count(children) > 0; + + let liveMessage = ''; + if (isLoading) { + liveMessage = 'Searching'; + } else if (hasValue && !hasChildren) { + liveMessage = 'No results found'; + } + const overlayState = useOverlayTriggerState({ defaultOpen }); + + // Close on interact outside — same pattern as ComboBox. + // isNonModal disables useOverlay's built-in useInteractOutside, + // so we add our own that filters out clicks on the trigger. + useInteractOutside({ + ref: popoverRef, + onInteractOutside: e => { + const target = e.target as Element; + if (triggerRef.current?.contains(target)) { + return; + } + overlayState.close(); + }, + isDisabled: !overlayState.isOpen, + }); + + return ( + + { + onInputChange?.(value); + if (value) { + overlayState.open(); + } else { + overlayState.close(); + } + }} + > + { + if (e.key === 'Enter' && !overlayState.isOpen && hasValue) { + e.preventDefault(); + overlayState.open(); + } + }} + > +
+ + + + + +
+
+ {/* isNonModal keeps the page interactive while the popover is open, + required for virtual focus (aria-activedescendant) to work correctly */} + + + + + isLoading ? ( +
Searching...
+ ) : ( + + ) + } + onAction={() => { + overlayState.close(); + }} + > + {children} +
+
+
+
+ + {liveMessage} + +
+
+ ); +} + +/** @public */ +export function SearchAutocompleteItem(props: SearchAutocompleteItemProps) { + const { ownProps, restProps } = useDefinition( + SearchAutocompleteItemDefinition, + props, + ); + const { classes, children } = ownProps; + + return ( + +
{children}
+
+ ); +} diff --git a/packages/ui/src/components/SearchAutocomplete/definition.ts b/packages/ui/src/components/SearchAutocomplete/definition.ts new file mode 100644 index 0000000000..4a2e47881d --- /dev/null +++ b/packages/ui/src/components/SearchAutocomplete/definition.ts @@ -0,0 +1,71 @@ +/* + * 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 { defineComponent } from '../../hooks/useDefinition'; +import type { + SearchAutocompleteOwnProps, + SearchAutocompleteItemOwnProps, +} from './types'; +import styles from './SearchAutocomplete.module.css'; + +/** + * Component definition for SearchAutocomplete. + * @public + */ +export const SearchAutocompleteDefinition = + defineComponent()({ + styles, + classNames: { + root: 'bui-SearchAutocomplete', + searchField: 'bui-SearchAutocompleteSearchField', + searchFieldInput: 'bui-SearchAutocompleteInput', + searchFieldClear: 'bui-SearchAutocompleteClear', + popover: 'bui-SearchAutocompletePopover', + inner: 'bui-SearchAutocompleteInner', + listBox: 'bui-SearchAutocompleteListBox', + loadingState: 'bui-SearchAutocompleteLoadingState', + emptyState: 'bui-SearchAutocompleteEmptyState', + }, + propDefs: { + 'aria-label': {}, + 'aria-labelledby': {}, + size: { dataAttribute: true, default: 'small' }, + placeholder: { default: 'Search' }, + inputValue: {}, + onInputChange: {}, + popoverWidth: {}, + popoverPlacement: {}, + children: {}, + isLoading: {}, + defaultOpen: {}, + className: {}, + style: {}, + }, + }); + +/** @internal */ +export const SearchAutocompleteItemDefinition = + defineComponent()({ + styles, + classNames: { + root: 'bui-SearchAutocompleteItem', + itemContent: 'bui-SearchAutocompleteItemContent', + }, + propDefs: { + children: {}, + className: {}, + }, + }); diff --git a/packages/ui/src/components/SearchAutocomplete/index.ts b/packages/ui/src/components/SearchAutocomplete/index.ts new file mode 100644 index 0000000000..8e15383008 --- /dev/null +++ b/packages/ui/src/components/SearchAutocomplete/index.ts @@ -0,0 +1,27 @@ +/* + * 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 { + SearchAutocomplete, + SearchAutocompleteItem, +} from './SearchAutocomplete'; +export { SearchAutocompleteDefinition } from './definition'; +export type { + SearchAutocompleteProps, + SearchAutocompleteItemProps, + SearchAutocompleteOwnProps, + SearchAutocompleteItemOwnProps, +} from './types'; diff --git a/packages/ui/src/components/SearchAutocomplete/types.ts b/packages/ui/src/components/SearchAutocomplete/types.ts new file mode 100644 index 0000000000..a5920d370c --- /dev/null +++ b/packages/ui/src/components/SearchAutocomplete/types.ts @@ -0,0 +1,101 @@ +/* + * 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 { + ListBoxItemProps as AriaListBoxItemProps, + PopoverProps as AriaPopoverProps, +} from 'react-aria-components'; +import type { ReactNode } from 'react'; +import type { Breakpoint } from '../../types'; + +/** @public */ +export type SearchAutocompleteOwnProps = { + /** + * The current value of the search input (controlled). + */ + inputValue?: string; + + /** + * Handler called when the search input value changes. + */ + onInputChange?: (value: string) => void; + + /** + * The size of the search input. + * @defaultValue 'small' + */ + size?: 'small' | 'medium' | Partial>; + + /** + * Accessible label for the search input. + */ + 'aria-label'?: string; + + /** + * ID of the element that labels the search input. + */ + 'aria-labelledby'?: string; + + /** + * The placeholder text for the search input. + */ + placeholder?: string; + + /** + * Width of the results popover. Accepts any CSS width value. + * When not set, the popover matches the input width. + */ + popoverWidth?: string; + + /** + * Placement of the results popover relative to the input. + * @defaultValue 'bottom start' + */ + popoverPlacement?: AriaPopoverProps['placement']; + + /** + * The result items to render inside the autocomplete. + */ + children?: ReactNode; + + /** + * Whether results are currently loading. + * When true, displays a loading indicator and announces the loading state to screen readers. + */ + isLoading?: boolean; + + /** + * Whether the results popover is open by default. + */ + defaultOpen?: boolean; + + className?: string; + style?: React.CSSProperties; +}; + +/** @public */ +export interface SearchAutocompleteProps extends SearchAutocompleteOwnProps {} + +/** @public */ +export type SearchAutocompleteItemOwnProps = { + children: ReactNode; + className?: string; +}; + +/** @public */ +export interface SearchAutocompleteItemProps + extends SearchAutocompleteItemOwnProps, + Omit {} diff --git a/packages/ui/src/definitions.ts b/packages/ui/src/definitions.ts index a07fff7462..79c5a8b884 100644 --- a/packages/ui/src/definitions.ts +++ b/packages/ui/src/definitions.ts @@ -52,6 +52,10 @@ 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/index.ts b/packages/ui/src/index.ts index 290ae2a80f..2bf478f5fb 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -51,6 +51,7 @@ 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/Select'; From c25532a7a1376995d234dab10a6bed1ba3efc4f4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 11:31:49 +0100 Subject: [PATCH 153/188] Add @backstage/frontend-dev-utils package Adds a new `@backstage/frontend-dev-utils` package for the new frontend system. It provides a minimal `createDevApp` helper for wiring up a development app from a `dev/` entry point. The app-visualizer plugin is updated to use this new package as the initial testing ground. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/add-frontend-dev-utils.md | 5 ++ .../app-visualizer-use-frontend-dev-utils.md | 5 ++ packages/frontend-dev-utils/.eslintrc.js | 1 + packages/frontend-dev-utils/README.md | 19 +++++ packages/frontend-dev-utils/catalog-info.yaml | 10 +++ packages/frontend-dev-utils/knip-report.md | 2 + packages/frontend-dev-utils/package.json | 64 ++++++++++++++++ packages/frontend-dev-utils/report.api.md | 18 +++++ .../src/createDevApp.test.tsx | 74 +++++++++++++++++++ .../frontend-dev-utils/src/createDevApp.tsx | 64 ++++++++++++++++ packages/frontend-dev-utils/src/index.ts | 23 ++++++ plugins/app-visualizer/dev/index.ts | 9 +-- plugins/app-visualizer/package.json | 2 +- yarn.lock | 28 ++++++- 14 files changed, 315 insertions(+), 9 deletions(-) create mode 100644 .changeset/add-frontend-dev-utils.md create mode 100644 .changeset/app-visualizer-use-frontend-dev-utils.md create mode 100644 packages/frontend-dev-utils/.eslintrc.js create mode 100644 packages/frontend-dev-utils/README.md create mode 100644 packages/frontend-dev-utils/catalog-info.yaml create mode 100644 packages/frontend-dev-utils/knip-report.md create mode 100644 packages/frontend-dev-utils/package.json create mode 100644 packages/frontend-dev-utils/report.api.md create mode 100644 packages/frontend-dev-utils/src/createDevApp.test.tsx create mode 100644 packages/frontend-dev-utils/src/createDevApp.tsx create mode 100644 packages/frontend-dev-utils/src/index.ts diff --git a/.changeset/add-frontend-dev-utils.md b/.changeset/add-frontend-dev-utils.md new file mode 100644 index 0000000000..9b4299734d --- /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. 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/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..be76ebdbe9 --- /dev/null +++ b/packages/frontend-dev-utils/package.json @@ -0,0 +1,64 @@ +{ + "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:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/plugin-app": "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..2dd3b38467 --- /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 { + createAppOptions?: Omit; + features: (FrontendFeature | FrontendFeatureLoader)[]; +} +``` 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..fd54ee26f3 --- /dev/null +++ b/packages/frontend-dev-utils/src/createDevApp.test.tsx @@ -0,0 +1,74 @@ +/* + * 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 { within, waitFor } from '@testing-library/react'; +import { mockApis } from '@backstage/test-utils'; +import { createDevApp } from './createDevApp'; +import { default as appPlugin } from '@backstage/plugin-app'; + +describe('createDevApp', () => { + afterEach(() => { + document.getElementById('root')?.remove(); + }); + + 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
, + }, + }), + ], + }); + + createDevApp({ + features: [ + appPlugin.withOverrides({ + extensions: [ + appPlugin + .getExtension('sign-in-page:app') + .override({ disabled: true }), + ], + }), + testPlugin, + ], + createAppOptions: { + advanced: { + configLoader: async () => ({ config: mockApis.config() }), + }, + }, + }); + + const body = within(document.body); + await waitFor( + () => { + expect(body.getByText('Test Plugin Page')).toBeDefined(); + }, + { timeout: 10000 }, + ); + }, 15000); +}); diff --git a/packages/frontend-dev-utils/src/createDevApp.tsx b/packages/frontend-dev-utils/src/createDevApp.tsx new file mode 100644 index 0000000000..dd5d7efc59 --- /dev/null +++ b/packages/frontend-dev-utils/src/createDevApp.tsx @@ -0,0 +1,64 @@ +/* + * 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 ReactDOM from 'react-dom/client'; + +/** + * Options for {@link createDevApp}. + * + * @public + */ +export interface CreateDevAppOptions { + /** + * The list of features to load in the dev app. + */ + features: (FrontendFeature | FrontendFeatureLoader)[]; + + /** + * Additional options to pass through to `createApp`. + */ + createAppOptions?: Omit; +} + +/** + * 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 app = createApp({ + ...options.createAppOptions, + features: options.features, + }); + + ReactDOM.createRoot(document.getElementById('root')!).render( + app.createRoot(), + ); +} diff --git a/packages/frontend-dev-utils/src/index.ts b/packages/frontend-dev-utils/src/index.ts new file mode 100644 index 0000000000..d93fa3d473 --- /dev/null +++ b/packages/frontend-dev-utils/src/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. + */ + +/** + * Utilities for developing Backstage frontend plugins using the new frontend system. + * + * @packageDocumentation + */ + +export { createDevApp, type CreateDevAppOptions } from './createDevApp'; 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 4c2a456f5b..bf71701775 100644 --- a/plugins/app-visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -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/yarn.lock b/yarn.lock index 90ac7fa1c1..b54ad39bba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3671,6 +3671,32 @@ __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:^" + "@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" @@ -4026,7 +4052,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" From 0b4ed8550d8f9b3c433917550cbcbba74648f788 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 11:50:57 +0100 Subject: [PATCH 154/188] Document createDevApp pattern for frontend plugin development Add documentation for the new `@backstage/frontend-dev-utils` package across three docs pages: the frontend system building plugins guide, the CLI build system reference, and the project structure overview. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- docs/contribute/project-structure.md | 5 ++++- docs/frontend-system/building-plugins/01-index.md | 15 +++++++++++++++ docs/tooling/cli/02-build-system.md | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/contribute/project-structure.md b/docs/contribute/project-structure.md index 372fb79844..f1be29c312 100644 --- a/docs/contribute/project-structure.md +++ b/docs/contribute/project-structure.md @@ -144,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/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index d9acf76e15..12558d5413 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 forward additional options to `createApp` from `@backstage/frontend-defaults` using the `createAppOptions` option. + +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/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 From 63248c824e7179de215760b0b0c2b09556497d84 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 12:05:00 +0100 Subject: [PATCH 155/188] Bypass sign-in page in createDevApp The dev app now automatically disables the sign-in page extension, so plugins under development don't require authentication. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/add-frontend-dev-utils.md | 2 +- packages/frontend-dev-utils/package.json | 4 ++-- .../frontend-dev-utils/src/createDevApp.test.tsx | 12 +----------- packages/frontend-dev-utils/src/createDevApp.tsx | 12 +++++++++++- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.changeset/add-frontend-dev-utils.md b/.changeset/add-frontend-dev-utils.md index 9b4299734d..2ba6c3e5b6 100644 --- a/.changeset/add-frontend-dev-utils.md +++ b/.changeset/add-frontend-dev-utils.md @@ -2,4 +2,4 @@ '@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. +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. diff --git a/packages/frontend-dev-utils/package.json b/packages/frontend-dev-utils/package.json index be76ebdbe9..84e515cee1 100644 --- a/packages/frontend-dev-utils/package.json +++ b/packages/frontend-dev-utils/package.json @@ -37,11 +37,11 @@ }, "dependencies": { "@backstage/frontend-defaults": "workspace:^", - "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/plugin-app": "workspace:^" }, "devDependencies": { "@backstage/cli": "workspace:^", - "@backstage/plugin-app": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^16.0.0", diff --git a/packages/frontend-dev-utils/src/createDevApp.test.tsx b/packages/frontend-dev-utils/src/createDevApp.test.tsx index fd54ee26f3..7f94c5543e 100644 --- a/packages/frontend-dev-utils/src/createDevApp.test.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.test.tsx @@ -21,7 +21,6 @@ import { import { within, waitFor } from '@testing-library/react'; import { mockApis } from '@backstage/test-utils'; import { createDevApp } from './createDevApp'; -import { default as appPlugin } from '@backstage/plugin-app'; describe('createDevApp', () => { afterEach(() => { @@ -46,16 +45,7 @@ describe('createDevApp', () => { }); createDevApp({ - features: [ - appPlugin.withOverrides({ - extensions: [ - appPlugin - .getExtension('sign-in-page:app') - .override({ disabled: true }), - ], - }), - testPlugin, - ], + features: [testPlugin], createAppOptions: { advanced: { configLoader: async () => ({ config: mockApis.config() }), diff --git a/packages/frontend-dev-utils/src/createDevApp.tsx b/packages/frontend-dev-utils/src/createDevApp.tsx index dd5d7efc59..078a9208cf 100644 --- a/packages/frontend-dev-utils/src/createDevApp.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.tsx @@ -19,6 +19,7 @@ import { FrontendFeatureLoader, } from '@backstage/frontend-plugin-api'; import { createApp, CreateAppOptions } from '@backstage/frontend-defaults'; +import appPlugin from '@backstage/plugin-app'; import ReactDOM from 'react-dom/client'; /** @@ -55,7 +56,16 @@ export interface CreateDevAppOptions { export function createDevApp(options: CreateDevAppOptions): void { const app = createApp({ ...options.createAppOptions, - features: options.features, + features: [ + appPlugin.withOverrides({ + extensions: [ + appPlugin + .getExtension('sign-in-page:app') + .override({ disabled: true }), + ], + }), + ...options.features, + ], }); ReactDOM.createRoot(document.getElementById('root')!).render( From db612a84ab3645965cdd75d5e40f46841c3a80c4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 12:14:59 +0100 Subject: [PATCH 156/188] Allow frontend-dev-utils in dependency verification exceptions Add @backstage/frontend-dev-utils to the except list for the rule that prevents web-library packages from depending on frontend-plugin packages, since it legitimately wraps @backstage/plugin-app for development environments. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- scripts/verify-local-dependencies.js | 1 + 1 file changed, 1 insertion(+) 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', From 8ea3899944311d498554bdf19f891f5fdd9ec262 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 12:49:20 +0100 Subject: [PATCH 157/188] Add BUI CSS to createDevApp Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/add-frontend-dev-utils.md | 2 +- packages/frontend-dev-utils/package.json | 3 ++- packages/frontend-dev-utils/src/createDevApp.tsx | 3 +++ yarn.lock | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.changeset/add-frontend-dev-utils.md b/.changeset/add-frontend-dev-utils.md index 2ba6c3e5b6..310ab204a0 100644 --- a/.changeset/add-frontend-dev-utils.md +++ b/.changeset/add-frontend-dev-utils.md @@ -2,4 +2,4 @@ '@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. +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. diff --git a/packages/frontend-dev-utils/package.json b/packages/frontend-dev-utils/package.json index 84e515cee1..33eadabbe2 100644 --- a/packages/frontend-dev-utils/package.json +++ b/packages/frontend-dev-utils/package.json @@ -38,7 +38,8 @@ "dependencies": { "@backstage/frontend-defaults": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", - "@backstage/plugin-app": "workspace:^" + "@backstage/plugin-app": "workspace:^", + "@backstage/ui": "workspace:^" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/frontend-dev-utils/src/createDevApp.tsx b/packages/frontend-dev-utils/src/createDevApp.tsx index 078a9208cf..54eea25d06 100644 --- a/packages/frontend-dev-utils/src/createDevApp.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.tsx @@ -14,6 +14,9 @@ * limitations under the License. */ +// eslint-disable-next-line @backstage/no-ui-css-imports-in-non-frontend +import '@backstage/ui/css/styles.css'; + import { FrontendFeature, FrontendFeatureLoader, diff --git a/yarn.lock b/yarn.lock index b54ad39bba..771285702f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3680,6 +3680,7 @@ __metadata: "@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" From 2e5c65120a76b6093c64ae7f8a0654a4b362752a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 15:00:02 +0100 Subject: [PATCH 158/188] Fix PluginHeader ResizeObserver loop Defer header height updates to avoid ResizeObserver loop warnings in FullPage layouts and normalize header actions before rendering. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../fix-plugin-header-resizeobserver-loop.md | 7 +++ .../components/PluginHeader/PluginHeader.tsx | 51 ++++++++++++++++--- 2 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 .changeset/fix-plugin-header-resizeobserver-loop.md diff --git a/.changeset/fix-plugin-header-resizeobserver-loop.md b/.changeset/fix-plugin-header-resizeobserver-loop.md new file mode 100644 index 0000000000..b91620d00c --- /dev/null +++ b/.changeset/fix-plugin-header-resizeobserver-loop.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Fixed `PluginHeader` to avoid triggering `ResizeObserver loop completed with undelivered notifications` warnings when used in layouts that react to the header height, such as pages that use `FullPage`. + +**Affected components:** PluginHeader diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.tsx b/packages/ui/src/components/PluginHeader/PluginHeader.tsx index ac5f04a011..d153a3af18 100644 --- a/packages/ui/src/components/PluginHeader/PluginHeader.tsx +++ b/packages/ui/src/components/PluginHeader/PluginHeader.tsx @@ -19,7 +19,7 @@ import { Tabs, TabList, Tab } from '../Tabs'; 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 { Box } from '../Box'; import { Link } from 'react-aria-components'; @@ -55,35 +55,70 @@ export const PluginHeader = (props: PluginHeaderProps) => { 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'); }; }, []); @@ -111,7 +146,7 @@ export const PluginHeader = (props: PluginHeaderProps) => {
- {customActions} + {actionChildren}
From 1594749d8eddd8f19641a1bf31ca8a7386a4dc13 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 15:06:05 +0100 Subject: [PATCH 159/188] Simplify createDevApp options Accept createApp options at the top level in createDevApp and update the tests, docs, API report, and changeset to match the new shape. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/add-frontend-dev-utils.md | 2 +- .../building-plugins/01-index.md | 2 +- packages/frontend-dev-utils/report.api.md | 4 +- .../src/createDevApp.test.tsx | 15 ++---- .../frontend-dev-utils/src/createDevApp.tsx | 46 +++++++++++-------- 5 files changed, 35 insertions(+), 34 deletions(-) diff --git a/.changeset/add-frontend-dev-utils.md b/.changeset/add-frontend-dev-utils.md index 310ab204a0..902dc8019a 100644 --- a/.changeset/add-frontend-dev-utils.md +++ b/.changeset/add-frontend-dev-utils.md @@ -2,4 +2,4 @@ '@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. +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 extends all `createApp` options from `@backstage/frontend-defaults` (except `features`), such as `bindRoutes` and `advanced`. diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index 12558d5413..b8e1346bee 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -121,7 +121,7 @@ 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 forward additional options to `createApp` from `@backstage/frontend-defaults` using the `createAppOptions` option. +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. The options also accept all other `createApp` options from `@backstage/frontend-defaults`, such as `bindRoutes` and `advanced`. 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. diff --git a/packages/frontend-dev-utils/report.api.md b/packages/frontend-dev-utils/report.api.md index 2dd3b38467..42261680ab 100644 --- a/packages/frontend-dev-utils/report.api.md +++ b/packages/frontend-dev-utils/report.api.md @@ -11,8 +11,8 @@ import { FrontendFeatureLoader } from '@backstage/frontend-plugin-api'; export function createDevApp(options: CreateDevAppOptions): void; // @public -export interface CreateDevAppOptions { - createAppOptions?: Omit; +export interface CreateDevAppOptions + extends Omit { features: (FrontendFeature | FrontendFeatureLoader)[]; } ``` diff --git a/packages/frontend-dev-utils/src/createDevApp.test.tsx b/packages/frontend-dev-utils/src/createDevApp.test.tsx index 7f94c5543e..ea377e2e2f 100644 --- a/packages/frontend-dev-utils/src/createDevApp.test.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.test.tsx @@ -18,7 +18,7 @@ import { PageBlueprint, createFrontendPlugin, } from '@backstage/frontend-plugin-api'; -import { within, waitFor } from '@testing-library/react'; +import { within } from '@testing-library/react'; import { mockApis } from '@backstage/test-utils'; import { createDevApp } from './createDevApp'; @@ -46,19 +46,12 @@ describe('createDevApp', () => { createDevApp({ features: [testPlugin], - createAppOptions: { - advanced: { - configLoader: async () => ({ config: mockApis.config() }), - }, + advanced: { + configLoader: async () => ({ config: mockApis.config() }), }, }); const body = within(document.body); - await waitFor( - () => { - expect(body.getByText('Test Plugin Page')).toBeDefined(); - }, - { timeout: 10000 }, - ); + await body.findByText('Test Plugin Page', {}, { timeout: 10000 }); }, 15000); }); diff --git a/packages/frontend-dev-utils/src/createDevApp.tsx b/packages/frontend-dev-utils/src/createDevApp.tsx index 54eea25d06..294b360040 100644 --- a/packages/frontend-dev-utils/src/createDevApp.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.tsx @@ -25,21 +25,32 @@ import { createApp, CreateAppOptions } from '@backstage/frontend-defaults'; import appPlugin from '@backstage/plugin-app'; import ReactDOM from 'react-dom/client'; +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, + }), + ], +}); + /** * Options for {@link createDevApp}. * * @public */ -export interface CreateDevAppOptions { +export interface CreateDevAppOptions + extends Omit { /** * The list of features to load in the dev app. */ features: (FrontendFeature | FrontendFeatureLoader)[]; - - /** - * Additional options to pass through to `createApp`. - */ - createAppOptions?: Omit; } /** @@ -57,19 +68,16 @@ export interface CreateDevAppOptions { * @public */ export function createDevApp(options: CreateDevAppOptions): void { - const app = createApp({ - ...options.createAppOptions, - features: [ - appPlugin.withOverrides({ - extensions: [ - appPlugin - .getExtension('sign-in-page:app') - .override({ disabled: true }), - ], - }), - ...options.features, - ], - }); + const { features, ...createAppOptions } = options; + const devFeatures: CreateAppOptions['features'] = [ + appPluginOverride, + ...features, + ]; + const appOptions: CreateAppOptions = { + ...createAppOptions, + features: devFeatures, + }; + const app = createApp(appOptions); ReactDOM.createRoot(document.getElementById('root')!).render( app.createRoot(), From 21ba6588f9bc817b76c12f7878593f0896e500a1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 15:10:19 +0100 Subject: [PATCH 160/188] Lazily load BUI CSS in createDevApp Move the BUI stylesheet into a lazily rendered helper so importing frontend-dev-utils no longer triggers global CSS side effects. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/frontend-dev-utils/src/BuiCss.tsx | 25 +++++++++++++++++++ .../frontend-dev-utils/src/createDevApp.tsx | 12 ++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 packages/frontend-dev-utils/src/BuiCss.tsx 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.tsx b/packages/frontend-dev-utils/src/createDevApp.tsx index 294b360040..7ec34b929b 100644 --- a/packages/frontend-dev-utils/src/createDevApp.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.tsx @@ -14,9 +14,6 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-ui-css-imports-in-non-frontend -import '@backstage/ui/css/styles.css'; - import { FrontendFeature, FrontendFeatureLoader, @@ -24,6 +21,7 @@ import { import { createApp, CreateAppOptions } from '@backstage/frontend-defaults'; import appPlugin from '@backstage/plugin-app'; import ReactDOM from 'react-dom/client'; +import { Suspense, lazy } from 'react'; type AppPluginWithSimpleOverrides = { withOverrides(options: { extensions: unknown[] }): FrontendFeature; @@ -40,6 +38,8 @@ const appPluginOverride = ( ], }); +const BuiCss = lazy(() => import('./BuiCss')); + /** * Options for {@link createDevApp}. * @@ -78,8 +78,12 @@ export function createDevApp(options: CreateDevAppOptions): void { features: devFeatures, }; const app = createApp(appOptions); + const AppRoot = app.createRoot(); ReactDOM.createRoot(document.getElementById('root')!).render( - app.createRoot(), + + + {AppRoot} + , ); } From 989a1dcc9bd14b601c02a4654b2299ac1380ca3a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 16:09:40 +0100 Subject: [PATCH 161/188] Restrict createDevApp options Limit createDevApp to features and bindRoutes so advanced createApp configuration stays out of the dev-app helper API. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/add-frontend-dev-utils.md | 2 +- .../building-plugins/01-index.md | 2 +- packages/frontend-dev-utils/report.api.md | 2 +- .../src/createDevApp.test.tsx | 17 +++++++++++++---- .../frontend-dev-utils/src/createDevApp.tsx | 6 +++--- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.changeset/add-frontend-dev-utils.md b/.changeset/add-frontend-dev-utils.md index 902dc8019a..987f02c6d9 100644 --- a/.changeset/add-frontend-dev-utils.md +++ b/.changeset/add-frontend-dev-utils.md @@ -2,4 +2,4 @@ '@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 extends all `createApp` options from `@backstage/frontend-defaults` (except `features`), such as `bindRoutes` and `advanced`. +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/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index b8e1346bee..861f454280 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -121,7 +121,7 @@ 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. The options also accept all other `createApp` options from `@backstage/frontend-defaults`, such as `bindRoutes` and `advanced`. +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. diff --git a/packages/frontend-dev-utils/report.api.md b/packages/frontend-dev-utils/report.api.md index 42261680ab..0f8f1dbd6a 100644 --- a/packages/frontend-dev-utils/report.api.md +++ b/packages/frontend-dev-utils/report.api.md @@ -12,7 +12,7 @@ export function createDevApp(options: CreateDevAppOptions): void; // @public export interface CreateDevAppOptions - extends Omit { + extends Pick { features: (FrontendFeature | FrontendFeatureLoader)[]; } ``` diff --git a/packages/frontend-dev-utils/src/createDevApp.test.tsx b/packages/frontend-dev-utils/src/createDevApp.test.tsx index ea377e2e2f..2dc4a69101 100644 --- a/packages/frontend-dev-utils/src/createDevApp.test.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.test.tsx @@ -19,11 +19,13 @@ import { createFrontendPlugin, } from '@backstage/frontend-plugin-api'; import { within } from '@testing-library/react'; -import { mockApis } from '@backstage/test-utils'; import { createDevApp } from './createDevApp'; +const anyEnv = (process.env = { ...process.env }) as any; + describe('createDevApp', () => { afterEach(() => { + delete anyEnv.APP_CONFIG; document.getElementById('root')?.remove(); }); @@ -44,11 +46,18 @@ describe('createDevApp', () => { ], }); + anyEnv.APP_CONFIG = [ + { + context: 'test', + data: { + app: { title: 'Test App' }, + backend: { baseUrl: 'http://localhost' }, + }, + }, + ]; + createDevApp({ features: [testPlugin], - advanced: { - configLoader: async () => ({ config: mockApis.config() }), - }, }); const body = within(document.body); diff --git a/packages/frontend-dev-utils/src/createDevApp.tsx b/packages/frontend-dev-utils/src/createDevApp.tsx index 7ec34b929b..09616e7286 100644 --- a/packages/frontend-dev-utils/src/createDevApp.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.tsx @@ -46,7 +46,7 @@ const BuiCss = lazy(() => import('./BuiCss')); * @public */ export interface CreateDevAppOptions - extends Omit { + extends Pick { /** * The list of features to load in the dev app. */ @@ -68,13 +68,13 @@ export interface CreateDevAppOptions * @public */ export function createDevApp(options: CreateDevAppOptions): void { - const { features, ...createAppOptions } = options; + const { features, bindRoutes } = options; const devFeatures: CreateAppOptions['features'] = [ appPluginOverride, ...features, ]; const appOptions: CreateAppOptions = { - ...createAppOptions, + bindRoutes, features: devFeatures, }; const app = createApp(appOptions); From ebeb0d4d8b272ce2e0aa9dd83117691c093fd3a7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 16:13:02 +0100 Subject: [PATCH 162/188] Use frontend-dev-utils in new frontend plugin template Update the new frontend plugin template to use createDevApp in its dev entry point and teach cli-module-new to resolve frontend-dev-utils versions for generated packages. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/new-frontend-plugin-template-dev-utils.md | 6 ++++++ packages/cli-module-new/src/lib/version.test.ts | 3 +++ packages/cli-module-new/src/lib/version.ts | 2 ++ packages/cli/templates/new-frontend-plugin/dev/index.tsx | 9 ++------- .../cli/templates/new-frontend-plugin/package.json.hbs | 2 +- 5 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 .changeset/new-frontend-plugin-template-dev-utils.md diff --git a/.changeset/new-frontend-plugin-template-dev-utils.md b/.changeset/new-frontend-plugin-template-dev-utils.md new file mode 100644 index 0000000000..5808a9307a --- /dev/null +++ b/.changeset/new-frontend-plugin-template-dev-utils.md @@ -0,0 +1,6 @@ +--- +'@backstage/cli': patch +'@backstage/cli-module-new': patch +--- + +Updated the new frontend plugin template to use `@backstage/frontend-dev-utils` in its `dev/` entry point instead of wiring `createApp` manually. Generated plugins now get the same dev app helper setup as the built-in examples. diff --git a/packages/cli-module-new/src/lib/version.test.ts b/packages/cli-module-new/src/lib/version.test.ts index 86b92e35c3..829a2f3d9e 100644 --- a/packages/cli-module-new/src/lib/version.test.ts +++ b/packages/cli-module-new/src/lib/version.test.ts @@ -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-module-new/src/lib/version.ts b/packages/cli-module-new/src/lib/version.ts index fd65135f4c..27e63bafdf 100644 --- a/packages/cli-module-new/src/lib/version.ts +++ b/packages/cli-module-new/src/lib/version.ts @@ -42,6 +42,7 @@ 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'; @@ -66,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, 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'}}", From b1033a1fc96fb5bc6346e52b1e6f4ef19b8f7334 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 16:45:18 +0100 Subject: [PATCH 163/188] Make createDevApp options explicit Define createDevApp options directly so the helper only exposes features and a typed bindRoutes property without inheriting from createApp options. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/frontend-dev-utils/report.api.md | 4 ++-- packages/frontend-dev-utils/src/createDevApp.tsx | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/frontend-dev-utils/report.api.md b/packages/frontend-dev-utils/report.api.md index 0f8f1dbd6a..39ca2aec2b 100644 --- a/packages/frontend-dev-utils/report.api.md +++ b/packages/frontend-dev-utils/report.api.md @@ -11,8 +11,8 @@ import { FrontendFeatureLoader } from '@backstage/frontend-plugin-api'; export function createDevApp(options: CreateDevAppOptions): void; // @public -export interface CreateDevAppOptions - extends Pick { +export interface CreateDevAppOptions { + bindRoutes?: CreateAppOptions['bindRoutes']; features: (FrontendFeature | FrontendFeatureLoader)[]; } ``` diff --git a/packages/frontend-dev-utils/src/createDevApp.tsx b/packages/frontend-dev-utils/src/createDevApp.tsx index 09616e7286..1f75e8d090 100644 --- a/packages/frontend-dev-utils/src/createDevApp.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.tsx @@ -45,12 +45,16 @@ const BuiCss = lazy(() => import('./BuiCss')); * * @public */ -export interface CreateDevAppOptions - extends Pick { +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']; } /** From a243b65b6256f068105e2992eb4a88c962b7edd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 16 Mar 2026 16:52:27 +0100 Subject: [PATCH 164/188] exit pre MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/pre.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 62ccda0410..634272b163 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "next", "initialVersions": { "example-app": "0.0.32", From 62d08492c7a29ed70e1038ab43ae005db32895f2 Mon Sep 17 00:00:00 2001 From: David Festal Date: Thu, 29 Jan 2026 16:51:10 +0100 Subject: [PATCH 165/188] feat(cli): contribute a `bundle` command for dynamic loading for both frontend and backend plugins. Signed-off-by: David Festal Assisted-by: Cursor --- .changeset/cli-package-bundle.md | 5 + docs/tooling/cli/03-commands.md | 126 +- packages/cli-module-build/cli-report.md | 17 + .../package/bundle/__fixtures__/.gitignore | 1 + .../backend/packages/foo-node/dist/index.js | 0 .../backend/packages/foo-node/package.json | 5 + .../backend/plugins/foo-backend/dist/index.js | 1 + .../backend/plugins/foo-backend/package.json | 8 + .../backend/plugins/foo-common/dist/index.js | 0 .../backend/plugins/foo-common/package.json | 5 + .../frontend/packages/foo-web/dist/index.js | 0 .../frontend/packages/foo-web/package.json | 5 + .../frontend/plugins/foo-react/dist/index.js | 0 .../frontend/plugins/foo-react/package.json | 5 + .../frontend/plugins/foo/dist/index.js | 0 .../frontend/plugins/foo/package.json | 8 + .../packed/frontend/dist/@mf-types/index.d.ts | 1 + .../packed/frontend/dist/index.js | 1 + .../packed/frontend/dist/remoteEntry.js | 1 + .../__fixtures__/packed/frontend/package.json | 33 + .../__fixtures__/packed/frontend/src/index.ts | 17 + .../commands/package/bundle/command.test.ts | 1126 +++++++++++++++++ .../src/commands/package/bundle/command.ts | 979 ++++++++++++++ .../src/commands/package/bundle/index.ts | 17 + packages/cli-module-build/src/index.ts | 9 + .../src/lib/packager/createDistWorkspace.ts | 123 +- .../src/lib/packager/index.ts | 6 +- packages/cli/cli-report.md | 17 + 28 files changed, 2479 insertions(+), 37 deletions(-) create mode 100644 .changeset/cli-package-bundle.md create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/.gitignore create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/packages/foo-node/dist/index.js create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/packages/foo-node/package.json create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-backend/dist/index.js create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-backend/package.json create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-common/dist/index.js create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-common/package.json create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/packages/foo-web/dist/index.js create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/packages/foo-web/package.json create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo-react/dist/index.js create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo-react/package.json create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo/dist/index.js create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo/package.json create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/dist/@mf-types/index.d.ts create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/dist/index.js create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/dist/remoteEntry.js create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/package.json create mode 100644 packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/src/index.ts create mode 100644 packages/cli-module-build/src/commands/package/bundle/command.test.ts create mode 100644 packages/cli-module-build/src/commands/package/bundle/command.ts create mode 100644 packages/cli-module-build/src/commands/package/bundle/index.ts diff --git a/.changeset/cli-package-bundle.md b/.changeset/cli-package-bundle.md new file mode 100644 index 0000000000..602d631fd1 --- /dev/null +++ b/.changeset/cli-package-bundle.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-module-build': minor +--- + +Added `package bundle` command to create self-contained plugin bundles for dynamic loading, to be used by the `backend-dynamic-feature-service`. Supports backend and frontend plugins, with optional `--pre-packed-dir` for batch bundling from a pre-built workspace. diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index 085175c958..6feb031277 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -38,6 +38,7 @@ The `package` command category, `yarn backstage-cli package --help`: ```text start [options] Start a package for local development build [options] Build a package for production deployment or publishing +bundle [options] Bundle a plugin for dynamic loading (backend or frontend) lint [options] [directories...] Lint a package test Run tests, forwarding args to Jest, defaulting to watch mode clean Delete cache directories @@ -204,6 +205,121 @@ Options: --module-federation Build a package as a module federation remote. Applies to frontend plugin packages only. ``` +## package bundle + +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 Structure + +The bundle is created in a directory named after the package. For example, +`@myorg/plugin-foo` creates `myorg-plugin-foo/` with the following structure: + +**Backend plugins:** + +```text +myorg-plugin-foo/ +├── package.json # Customized for dynamic loading +├── dist/ # Built plugin code +│ ├── index.cjs.js +│ └── .config-schema.json # Config schema (local packages only) +├── embedded/ # Embedded workspace packages (if any) +│ └── myorg-plugin-foo-common/ +│ ├── package.json +│ └── dist/ +└── node_modules/ # All production dependencies +``` + +**Frontend plugins** produce module federation assets at the bundle root (no `embedded/` or `node_modules/` unless `--pre-packed-dir` is used). + +The `.config-schema.json` contains merged config schemas from the plugin and any +embedded workspace packages. It excludes schemas from npm dependencies, as those +should be provided by the host application. The `package.json` is updated to +reference this generated JSON file instead of any original `.d.ts` schema. + +### 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 @@ -414,9 +530,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 diff --git a/packages/cli-module-build/cli-report.md b/packages/cli-module-build/cli-report.md index 1c11022dc8..961cd69c28 100644 --- a/packages/cli-module-build/cli-report.md +++ b/packages/cli-module-build/cli-report.md @@ -38,6 +38,7 @@ Options: Commands: build + bundle clean help [command] postpack @@ -60,6 +61,22 @@ Options: -h, --help ``` +### `backstage-cli-module-build package bundle` + +``` +Usage: @backstage/cli-module-build package bundle + +Options: + --clean + --no-build + --no-install + --output-destination + --output-name + --pre-packed-dir + --verbose + -h, --help +``` + ### `backstage-cli-module-build package clean` ``` 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-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/src/index.ts b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/src/index.ts new file mode 100644 index 0000000000..e512e4ba83 --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/src/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 {}; 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-module-build/src/index.ts b/packages/cli-module-build/src/index.ts index 887a4b562c..4b6a0b31c7 100644 --- a/packages/cli-module-build/src/index.ts +++ b/packages/cli-module-build/src/index.ts @@ -33,6 +33,15 @@ export default createCliModule({ 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.', + execute: { loader: () => import('./commands/package/bundle') }, + }); + reg.addCommand({ path: ['package', 'start'], description: 'Start a package for local development', diff --git a/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts index 535d54f9dc..72a06af69f 100644 --- a/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts +++ b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts @@ -38,6 +38,7 @@ import { } from '../builder'; import { productionPack } from './productionPack'; import { + BackstagePackage, PackageRoles, PackageGraph, PackageGraphNode, @@ -109,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}`); } }; } @@ -131,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 ?? []; @@ -168,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 }); @@ -182,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 }); @@ -190,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') @@ -227,8 +230,8 @@ export async function createDistWorkspace( 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(); }, }); @@ -240,6 +243,7 @@ export async function createDistWorkspace( targets, Boolean(options.alwaysPack), Boolean(options.enableFeatureDetection), + logger, ); const files: FileEntry[] = options.files ?? ['yarn.lock', 'package.json']; @@ -270,7 +274,7 @@ export async function createDistWorkspace( ); } - return targetDir; + return { targetDir, targets }; } const FAST_PACK_SCRIPTS = [ @@ -279,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, @@ -300,7 +333,7 @@ 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(targetPaths.rootDir, target.dir); const absoluteOutputPath = resolvePath(workspaceDir, outputDir); @@ -315,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(targetPaths.rootDir, 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. @@ -372,3 +400,28 @@ async function moveToDistWorkspace( }, }); } + +/** + * 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 index 75f3fdf71d..4b382d0340 100644 --- a/packages/cli-module-build/src/lib/packager/index.ts +++ b/packages/cli-module-build/src/lib/packager/index.ts @@ -14,4 +14,8 @@ * limitations under the License. */ -export { createDistWorkspace } from './createDistWorkspace'; +export { + createDistWorkspace, + packToDirectory, + resolveLocalDependencies, +} from './createDistWorkspace'; diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index bb22326786..15917c0280 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -317,6 +317,7 @@ Options: Commands: build + bundle clean help [command] lint @@ -341,6 +342,22 @@ Options: -h, --help ``` +### `backstage-cli package bundle` + +``` +Usage: backstage-cli package bundle + +Options: + --clean + --no-build + --no-install + --output-destination + --output-name + --pre-packed-dir + --verbose + -h, --help +``` + ### `backstage-cli package clean` ``` From 551d729723ec3d7b70ac89add4da703915affa2e Mon Sep 17 00:00:00 2001 From: David Festal Date: Wed, 4 Mar 2026 18:03:32 +0100 Subject: [PATCH 166/188] docs(backend-dynamic-feature-service): update README to mention the `bundle` command. Signed-off-by: David Festal Assisted-by: Cursor Signed-off-by: David Festal --- .../backend-dynamic-feature-service/README.md | 64 +++++++++++-------- 1 file changed, 38 insertions(+), 26 deletions(-) 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 From 4074a227c25c6d859c2b9dc387f79d9c55c1438a Mon Sep 17 00:00:00 2001 From: David Festal Date: Tue, 3 Mar 2026 22:23:14 +0100 Subject: [PATCH 167/188] fix(backend-dynamic-feature-service): improve package resolution for bundled dynamic plugins Updated `resolvePackagePath` to correctly resolve `package.json` for dynamic plugins that bundle their own copy of `@backstage/backend-plugin-api`. This change ensures that the resolution works for both the host application and bundled dependencies, enhancing compatibility with plugins created using the `backstage-cli package bundle` command. Signed-off-by: David Festal Assisted-by: Cursor --- .changeset/brave-pens-argue.md | 5 +++ docs/tooling/cli/03-commands.md | 6 ++- .../dist/index.cjs.js | 28 +++++++++++++ .../@backstage/backend-plugin-api/index.js | 22 +++++++++++ .../backend-plugin-api/package.json | 6 +++ .../test-backend-bundled-dynamic/package.json | 31 +++++++++++++++ .../src/features/features.test.ts | 39 +++++++++++++++++++ .../src/loader/CommonJSModuleLoader.ts | 20 ++++++---- packages/cli-module-build/cli-report.md | 17 -------- packages/cli-module-build/src/index.ts | 3 +- packages/cli/cli-report.md | 17 -------- 11 files changed, 151 insertions(+), 43 deletions(-) create mode 100644 .changeset/brave-pens-argue.md create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/dist/index.cjs.js create mode 100644 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 create mode 100644 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 create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/package.json 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/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index 6feb031277..38bb16b3c4 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -38,7 +38,6 @@ The `package` command category, `yarn backstage-cli package --help`: ```text start [options] Start a package for local development build [options] Build a package for production deployment or publishing -bundle [options] Bundle a plugin for dynamic loading (backend or frontend) lint [options] [directories...] Lint a package test Run tests, forwarding args to Jest, defaulting to watch mode clean Delete cache directories @@ -207,6 +206,11 @@ Options: ## 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. 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/cli-module-build/cli-report.md b/packages/cli-module-build/cli-report.md index 961cd69c28..1c11022dc8 100644 --- a/packages/cli-module-build/cli-report.md +++ b/packages/cli-module-build/cli-report.md @@ -38,7 +38,6 @@ Options: Commands: build - bundle clean help [command] postpack @@ -61,22 +60,6 @@ Options: -h, --help ``` -### `backstage-cli-module-build package bundle` - -``` -Usage: @backstage/cli-module-build package bundle - -Options: - --clean - --no-build - --no-install - --output-destination - --output-name - --pre-packed-dir - --verbose - -h, --help -``` - ### `backstage-cli-module-build package clean` ``` diff --git a/packages/cli-module-build/src/index.ts b/packages/cli-module-build/src/index.ts index 4b6a0b31c7..cf892db53c 100644 --- a/packages/cli-module-build/src/index.ts +++ b/packages/cli-module-build/src/index.ts @@ -38,7 +38,8 @@ export default createCliModule({ 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.', + 'Supports both backend and frontend plugins. Experimental.', + experimental: true, execute: { loader: () => import('./commands/package/bundle') }, }); diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index 15917c0280..bb22326786 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -317,7 +317,6 @@ Options: Commands: build - bundle clean help [command] lint @@ -342,22 +341,6 @@ Options: -h, --help ``` -### `backstage-cli package bundle` - -``` -Usage: backstage-cli package bundle - -Options: - --clean - --no-build - --no-install - --output-destination - --output-name - --pre-packed-dir - --verbose - -h, --help -``` - ### `backstage-cli package clean` ``` From c19c2885419ae1aa4827bb05588d7bb2ad168f2f Mon Sep 17 00:00:00 2001 From: David Festal Date: Tue, 10 Mar 2026 19:46:11 +0100 Subject: [PATCH 168/188] review(cli): refine the `bundle` command output expectations Signed-off-by: David Festal Assisted-by: Cursor --- docs/tooling/cli/03-commands.md | 45 ++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index 38bb16b3c4..2ed0e91891 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -280,32 +280,35 @@ Options: plugins, this also enables yarn.lock generation for SBOM. ``` -### Output Structure +### Output Contract -The bundle is created in a directory named after the package. For example, -`@myorg/plugin-foo` creates `myorg-plugin-foo/` with the following structure: +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: -**Backend plugins:** +**All bundles:** -```text -myorg-plugin-foo/ -├── package.json # Customized for dynamic loading -├── dist/ # Built plugin code -│ ├── index.cjs.js -│ └── .config-schema.json # Config schema (local packages only) -├── embedded/ # Embedded workspace packages (if any) -│ └── myorg-plugin-foo-common/ -│ ├── package.json -│ └── dist/ -└── node_modules/ # All production dependencies -``` +- 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`. -**Frontend plugins** produce module federation assets at the bundle root (no `embedded/` or `node_modules/` unless `--pre-packed-dir` is used). +**Backend plugins** (`backend-plugin`, `backend-plugin-module`): -The `.config-schema.json` contains merged config schemas from the plugin and any -embedded workspace packages. It excludes schemas from npm dependencies, as those -should be provided by the host application. The `package.json` is updated to -reference this generated JSON file instead of any original `.d.ts` schema. +- 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 From a67b79bb2113f9194239719cf88990630ded1377 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 18:54:04 +0100 Subject: [PATCH 169/188] Support React 17 rendering in createDevApp. Align the dev app render path with the existing dev-utils helper, add a clearer missing-root error, and extend the tests to cover bindRoutes forwarding and env cleanup. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/createDevApp.test.tsx | 126 +++++++++++++++++- .../frontend-dev-utils/src/createDevApp.tsx | 44 +++++- 2 files changed, 157 insertions(+), 13 deletions(-) diff --git a/packages/frontend-dev-utils/src/createDevApp.test.tsx b/packages/frontend-dev-utils/src/createDevApp.test.tsx index 2dc4a69101..b5beb79c1c 100644 --- a/packages/frontend-dev-utils/src/createDevApp.test.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.test.tsx @@ -18,15 +18,25 @@ import { PageBlueprint, createFrontendPlugin, } from '@backstage/frontend-plugin-api'; -import { within } from '@testing-library/react'; +import { waitFor, within } from '@testing-library/react'; import { createDevApp } from './createDevApp'; -const anyEnv = (process.env = { ...process.env }) as any; +jest.setTimeout(15000); + +const originalEnv = process.env; describe('createDevApp', () => { + beforeEach(() => { + process.env = { ...originalEnv }; + }); + afterEach(() => { - delete anyEnv.APP_CONFIG; - document.getElementById('root')?.remove(); + document.body.innerHTML = ''; + jest.resetAllMocks(); + }); + + afterAll(() => { + process.env = originalEnv; }); it('should render a dev app with a plugin', async () => { @@ -46,7 +56,7 @@ describe('createDevApp', () => { ], }); - anyEnv.APP_CONFIG = [ + (process.env as any).APP_CONFIG = [ { context: 'test', data: { @@ -62,5 +72,109 @@ describe('createDevApp', () => { const body = within(document.body); await body.findByText('Test Plugin Page', {}, { timeout: 10000 }); - }, 15000); + }); + + 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); + + let isolatedCreateDevApp: typeof import('./createDevApp').createDevApp; + jest.isolateModules(() => { + ({ createDevApp: isolatedCreateDevApp } = require('./createDevApp')); + }); + + isolatedCreateDevApp({ + bindRoutes, + features: ['plugin-feature'] as any, + }); + + await waitFor(() => { + expect(createApp).toHaveBeenCalledWith({ + bindRoutes, + features: ['app-plugin-override', 'plugin-feature'], + }); + expect(createRoot).toHaveBeenCalledWith(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); + + let isolatedCreateDevApp: typeof import('./createDevApp').createDevApp; + jest.isolateModules(() => { + ({ createDevApp: isolatedCreateDevApp } = require('./createDevApp')); + }); + + 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 index 1f75e8d090..112901ed52 100644 --- a/packages/frontend-dev-utils/src/createDevApp.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.tsx @@ -20,8 +20,8 @@ import { } from '@backstage/frontend-plugin-api'; import { createApp, CreateAppOptions } from '@backstage/frontend-defaults'; import appPlugin from '@backstage/plugin-app'; -import ReactDOM from 'react-dom/client'; import { Suspense, lazy } from 'react'; +import 'react-dom'; type AppPluginWithSimpleOverrides = { withOverrides(options: { extensions: unknown[] }): FrontendFeature; @@ -40,6 +40,15 @@ const appPluginOverride = ( 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}. * @@ -57,6 +66,18 @@ export interface CreateDevAppOptions { 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. * @@ -72,6 +93,7 @@ export interface CreateDevAppOptions { * @public */ export function createDevApp(options: CreateDevAppOptions): void { + const rootElement = getRootElement(); const { features, bindRoutes } = options; const devFeatures: CreateAppOptions['features'] = [ appPluginOverride, @@ -84,10 +106,18 @@ export function createDevApp(options: CreateDevAppOptions): void { const app = createApp(appOptions); const AppRoot = app.createRoot(); - ReactDOM.createRoot(document.getElementById('root')!).render( - - - {AppRoot} - , - ); + ReactDOMPromise.then(ReactDOM => { + const rootNode = ( + + + {AppRoot} + + ); + + if ('createRoot' in ReactDOM) { + ReactDOM.createRoot(rootElement).render(rootNode); + } else { + ReactDOM.render(rootNode, rootElement); + } + }); } From 42f8c9b2b8e8df353fdd0ae042461094cea80572 Mon Sep 17 00:00:00 2001 From: Johan Persson Date: Mon, 16 Mar 2026 18:56:24 +0100 Subject: [PATCH 170/188] feat(ui): centralize routing in BUIProvider (#33267) * feat(ui): centralize routing in BUIProvider BUIProvider now auto-detects React Router context and provides client-side navigation for all BUI components. Retired InternalLinkProvider and added BUIRouterProvider as a public export for integration use. Signed-off-by: Johan Persson * feat(plugin-app): move BUIProvider inside app router Moved BUIProvider from wrapping AppRouter to being a child inside it, so it detects the React Router context and provides client-side routing for all BUI components. Signed-off-by: Johan Persson * feat(core-app-api): add BUIRouterProvider to legacy app router Added BUIRouterProvider inside the legacy AppRouter to provide React Aria routing for all BUI components. Signed-off-by: Johan Persson * docs(ui): update BUIProvider documentation for routing Updated installation docs to cover BUIProvider's routing role and the requirement to render it inside a React Router context. Signed-off-by: Johan Persson * refactor(ui): move BUIProvider from analytics to provider directory BUIProvider now handles both analytics and routing, so it no longer belongs in the analytics directory. Signed-off-by: Johan Persson * fix(ui): add BUIProvider to storybook stories with MemoryRouter Added BUIProvider inside MemoryRouter in all stories that use routing, so client-side navigation works in Storybook. Signed-off-by: Johan Persson * fix(plugin-app): move BUIProvider inside RouterComponent Moved BUIProvider to wrap all content inside RouterComponent so that extraElements (like dialogs) also get BUI context. Signed-off-by: Johan Persson * refactor: replace BUIRouterProvider with BUIProvider in legacy app Use BUIProvider directly inside the legacy AppRouter instead of a separate BUIRouterProvider export. Removes BUIRouterProvider from the public API of @backstage/ui. Signed-off-by: Johan Persson * refactor(ui): inline routing logic into BUIProvider Removed the routing/ directory and inlined the RouterProvider setup directly into BUIProvider since it's the only consumer. Signed-off-by: Johan Persson --------- Signed-off-by: Johan Persson --- .changeset/ninety-onions-ask.md | 5 + .changeset/polite-trains-crash.md | 23 + .changeset/small-feet-arrive.md | 5 + .../src/app/get-started/installation/page.mdx | 17 +- .../app/get-started/installation/snippets.ts | 11 +- packages/core-app-api/src/app/AppRouter.tsx | 54 +- packages/ui/src/analytics/index.ts | 2 - .../ButtonLink/ButtonLink.stories.tsx | 5 +- .../src/components/ButtonLink/ButtonLink.tsx | 29 +- .../components/FullPage/FullPage.stories.tsx | 5 +- .../src/components/Header/Header.stories.tsx | 142 ++-- .../InternalLinkProvider.tsx | 157 ---- .../ui/src/components/Link/Link.stories.tsx | 5 +- packages/ui/src/components/Link/Link.tsx | 7 +- .../ui/src/components/Menu/Menu.stories.tsx | 5 +- packages/ui/src/components/Menu/Menu.tsx | 112 ++- .../Menu/MenuAutocomplete.stories.tsx | 5 +- .../Menu/MenuAutocompleteListBox.stories.tsx | 5 +- .../components/Menu/MenuListBox.stories.tsx | 5 +- .../PluginHeader/PluginHeader.stories.tsx | 216 ++--- .../SearchField/SearchField.stories.tsx | 9 +- .../src/components/Table/components/Row.tsx | 27 +- .../ui/src/components/Table/stories/utils.tsx | 5 +- .../ui/src/components/Tabs/Tabs.stories.tsx | 759 +++++++++--------- packages/ui/src/components/Tabs/Tabs.tsx | 39 +- .../components/TagGroup/TagGroup.stories.tsx | 5 +- .../ui/src/components/TagGroup/TagGroup.tsx | 26 +- .../src/guidelines/CardsWithTable.stories.tsx | 5 +- packages/ui/src/index.ts | 7 +- .../{analytics => provider}/BUIProvider.tsx | 26 +- .../index.ts | 12 +- .../utils/{isExternalLink.ts => linkUtils.ts} | 9 + plugins/app/src/extensions/AppRoot.tsx | 54 +- 33 files changed, 884 insertions(+), 914 deletions(-) create mode 100644 .changeset/ninety-onions-ask.md create mode 100644 .changeset/polite-trains-crash.md create mode 100644 .changeset/small-feet-arrive.md delete mode 100644 packages/ui/src/components/InternalLinkProvider/InternalLinkProvider.tsx rename packages/ui/src/{analytics => provider}/BUIProvider.tsx (68%) rename packages/ui/src/{components/InternalLinkProvider => provider}/index.ts (68%) rename packages/ui/src/utils/{isExternalLink.ts => linkUtils.ts} (85%) diff --git a/.changeset/ninety-onions-ask.md b/.changeset/ninety-onions-ask.md new file mode 100644 index 0000000000..95798043c9 --- /dev/null +++ b/.changeset/ninety-onions-ask.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +Added `BUIProvider` inside the legacy app router to enable client-side routing for all BUI components. diff --git a/.changeset/polite-trains-crash.md b/.changeset/polite-trains-crash.md new file mode 100644 index 0000000000..8926f11e94 --- /dev/null +++ b/.changeset/polite-trains-crash.md @@ -0,0 +1,23 @@ +--- +'@backstage/ui': minor +--- + +**BREAKING**: Centralized client-side routing in `BUIProvider`. Components like Link, ButtonLink, Tabs, Menu, TagGroup, and Table now require a `BUIProvider` rendered inside a React Router context for client-side navigation to work. + +**Migration:** + +This change requires updating `@backstage/plugin-app` and `@backstage/core-app-api` alongside `@backstage/ui`. If you only upgrade `@backstage/ui`, BUI components will fall back to full-page navigation. + +If you cannot upgrade all packages together, or if you have a custom app shell, add a `BUIProvider` inside your Router: + +```diff ++ import { BUIProvider } from '@backstage/ui'; + + ++ + ++ + +``` + +**Affected components:** Link, ButtonLink, Tabs, Menu, TagGroup, Table diff --git a/.changeset/small-feet-arrive.md b/.changeset/small-feet-arrive.md new file mode 100644 index 0000000000..a0ed7b1b01 --- /dev/null +++ b/.changeset/small-feet-arrive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app': patch +--- + +Moved `BUIProvider` inside the app router to enable automatic client-side routing for all BUI components. diff --git a/docs-ui/src/app/get-started/installation/page.mdx b/docs-ui/src/app/get-started/installation/page.mdx index c5421ae9d7..d87c2bcba4 100644 --- a/docs-ui/src/app/get-started/installation/page.mdx +++ b/docs-ui/src/app/get-started/installation/page.mdx @@ -49,23 +49,28 @@ your plugin and import the components you need. -## Analytics +## BUIProvider -BUI components with navigation behavior — Link, ButtonLink, Tab, MenuItem, Tag, and Row — can fire analytics events when clicked. To enable this, you need to connect BUI's analytics layer to Backstage's analytics system. +`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**, analytics is wired automatically via `@backstage/plugin-app` — no setup required. +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` and pass in Backstage's `useAnalytics` hook: +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: -### How it works + -Once configured, BUI components 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. +### 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 12dfff4f44..61edb266b3 100644 --- a/docs-ui/src/app/get-started/installation/snippets.ts +++ b/docs-ui/src/app/get-started/installation/snippets.ts @@ -7,11 +7,14 @@ export const snippet = `import { Flex, Button, Text } from '@backstage/ui'; export const analyticsSetupSnippet = `import { BUIProvider } from '@backstage/ui'; import { useAnalytics } from '@backstage/core-plugin-api'; +import { BrowserRouter } from 'react-router-dom'; -// Wrap your app content with the provider - - -`; +// BUIProvider must be inside a Router for client-side navigation + + + + +`; export const analyticsNoTrackSnippet = `// Suppress analytics for a specific link 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/ui/src/analytics/index.ts b/packages/ui/src/analytics/index.ts index a9b073afa7..de42ed3181 100644 --- a/packages/ui/src/analytics/index.ts +++ b/packages/ui/src/analytics/index.ts @@ -15,8 +15,6 @@ */ export { useAnalytics } from './useAnalytics'; -export { BUIProvider } from './BUIProvider'; -export type { BUIProviderProps } from './BUIProvider'; export { getNodeText } from './getNodeText'; export type { AnalyticsTracker, 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 96f477a4e8..4d6906f32f 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.tsx +++ b/packages/ui/src/components/ButtonLink/ButtonLink.tsx @@ -19,7 +19,6 @@ 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 */ @@ -43,21 +42,19 @@ export const ButtonLink = forwardRef( }; return ( - - - - {iconStart} - {children} - {iconEnd} - - - + + + {iconStart} + {children} + {iconEnd} + + ); }, ); diff --git a/packages/ui/src/components/FullPage/FullPage.stories.tsx b/packages/ui/src/components/FullPage/FullPage.stories.tsx index 05169ca8ea..6adfaa7efb 100644 --- a/packages/ui/src/components/FullPage/FullPage.stories.tsx +++ b/packages/ui/src/components/FullPage/FullPage.stories.tsx @@ -22,6 +22,7 @@ import { Container } from '../Container'; import { Text } from '../Text'; import type { HeaderTab } from '../PluginHeader/types'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; const meta = preview.meta({ title: 'Backstage UI/FullPage', @@ -33,7 +34,9 @@ const meta = preview.meta({ const withRouter = (Story: StoryFn) => ( - + + + ); diff --git a/packages/ui/src/components/Header/Header.stories.tsx b/packages/ui/src/components/Header/Header.stories.tsx index e605f2ed58..0adfb43d55 100644 --- a/packages/ui/src/components/Header/Header.stories.tsx +++ b/packages/ui/src/components/Header/Header.stories.tsx @@ -19,6 +19,7 @@ import type { StoryFn } from '@storybook/react-vite'; import { Header } from './Header'; import type { HeaderTab } from '../PluginHeader/types'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; import { Button, Container, @@ -88,7 +89,9 @@ const menuItems = [ const withRouter = (Story: StoryFn) => ( - + + + ); @@ -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/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 c0de9c3087..a4509576c9 100644 --- a/packages/ui/src/components/Link/Link.tsx +++ b/packages/ui/src/components/Link/Link.tsx @@ -19,7 +19,6 @@ import { useLink } from 'react-aria'; import type { LinkProps } from './types'; import { useDefinition } from '../../hooks/useDefinition'; import { LinkDefinition } from './definition'; -import { InternalLinkProvider } from '../InternalLinkProvider'; import { getNodeText } from '../../analytics/getNodeText'; const LinkInternal = forwardRef((props, ref) => { @@ -64,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/Menu/Menu.stories.tsx b/packages/ui/src/components/Menu/Menu.stories.tsx index 982055ade1..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 => ( - + + + ), ], diff --git a/packages/ui/src/components/Menu/Menu.tsx b/packages/ui/src/components/Menu/Menu.tsx index 5ab55cf7d8..cb6028c45a 100644 --- a/packages/ui/src/components/Menu/Menu.tsx +++ b/packages/ui/src/components/Menu/Menu.tsx @@ -62,17 +62,11 @@ import { RiCheckLine, RiCloseCircleLine, } from '@remixicon/react'; -import { - isInternalLink, - createRoutingRegistration, -} from '../InternalLinkProvider'; +import { isInternalLink } from '../../utils/linkUtils'; import { getNodeText } from '../../analytics/getNodeText'; import { Box } from '../Box'; import { BgReset } from '../../hooks/useBg'; -const { RoutingProvider, useRoutingRegistrationEffect } = - createRoutingRegistration(); - // The height will be used for virtualized menus. It should match the size set in CSS for each menu item. const rowHeight = 32; @@ -110,26 +104,24 @@ export const Menu = (props: MenuProps) => { ); return ( - - - - - {virtualized ? ( - - {menuContent} - - ) : ( - menuContent - )} - - - - + + + + {virtualized ? ( + + {menuContent} + + ) : ( + menuContent + )} + + + ); }; @@ -206,40 +198,38 @@ export const MenuAutocomplete = (props: MenuAutocompleteProps) => { ); return ( - - - - - - + + + + + + + + + + {virtualized ? ( + - - - - - - {virtualized ? ( - - {menuContent} - - ) : ( - menuContent - )} - - - - - + {menuContent} + + ) : ( + menuContent + )} + + + + ); }; @@ -318,8 +308,6 @@ export const MenuItem = (props: MenuItemProps) => { ); const { classes, iconStart, children, href } = ownProps; - useRoutingRegistrationEffect(href); - const handleAction = () => { if (href) { const text = 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/PluginHeader/PluginHeader.stories.tsx b/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx index 151c48aab6..d0d3475dbb 100644 --- a/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx +++ b/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx @@ -29,6 +29,7 @@ import { MenuItem, } from '../../'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; import { RiHeartLine, RiEmotionHappyLine, @@ -47,7 +48,9 @@ const meta = preview.meta({ const withRouter = (Story: StoryFn) => ( - + + + ); @@ -336,16 +339,18 @@ export const WithMockedURLCampaigns = meta.story({ }, render: args => ( - - - - 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/SearchField/SearchField.stories.tsx b/packages/ui/src/components/SearchField/SearchField.stories.tsx index 70784f3206..380b55e580 100644 --- a/packages/ui/src/components/SearchField/SearchField.stories.tsx +++ b/packages/ui/src/components/SearchField/SearchField.stories.tsx @@ -27,6 +27,7 @@ import { RiCactusLine, RiEBike2Line } from '@remixicon/react'; import { Button } from '../Button'; import { PluginHeader } from '../PluginHeader'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; const meta = preview.meta({ title: 'Backstage UI/SearchField', @@ -188,7 +189,9 @@ export const InHeader = meta.story({ decorators: [ Story => ( - + + + ), ], @@ -225,7 +228,9 @@ export const StartCollapsedInHeader = meta.story({ decorators: [ Story => ( - + + + ), ], diff --git a/packages/ui/src/components/Table/components/Row.tsx b/packages/ui/src/components/Table/components/Row.tsx index 75b715f7f7..9d4a15f601 100644 --- a/packages/ui/src/components/Table/components/Row.tsx +++ b/packages/ui/src/components/Table/components/Row.tsx @@ -24,8 +24,7 @@ import { Checkbox } from '../../Checkbox'; import { useDefinition } from '../../../hooks/useDefinition'; import { RowDefinition } from '../definition'; import type { RowProps } from '../types'; -import { isExternalLink } from '../../../utils/isExternalLink'; -import { InternalLinkProvider } from '../../InternalLinkProvider'; +import { isExternalLink } from '../../../utils/linkUtils'; import clsx from 'clsx'; import { Flex } from '../../Flex'; @@ -85,18 +84,16 @@ export function Row(props: RowProps) { ); return ( - - - {content} - - + + {content} + ); } 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/Tabs/Tabs.stories.tsx b/packages/ui/src/components/Tabs/Tabs.stories.tsx index 55cdf7c446..99bf75a072 100644 --- a/packages/ui/src/components/Tabs/Tabs.stories.tsx +++ b/packages/ui/src/components/Tabs/Tabs.stories.tsx @@ -18,6 +18,7 @@ import preview from '../../../../../.storybook/preview'; import type { StoryFn } from '@storybook/react-vite'; import { Tabs, TabList, Tab, TabPanel } from './Tabs'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; import { Box } from '../Box'; import { Text } from '../Text'; @@ -28,7 +29,9 @@ const meta = preview.meta({ const withRouter = (Story: StoryFn) => ( - + + + ); @@ -79,28 +82,30 @@ export const WithMockedURLTab2 = meta.story({ }, render: () => ( - - - - Tab 1 - - - Tab 2 - - - Tab 3 With long title - - - - - - Current URL is mocked to be: /tab2 - - - Notice how the "Tab 2" tab is selected (highlighted) because it - matches the current path. - - + + + + + Tab 1 + + + Tab 2 + + + Tab 3 With long title + + + + + + Current URL is mocked to be: /tab2 + + + Notice how the "Tab 2" tab is selected (highlighted) because it + matches the current path. + + + ), }); @@ -111,28 +116,30 @@ export const WithMockedURLTab3 = meta.story({ }, render: () => ( - - - - Tab 1 - - - Tab 2 - - - Tab 3 With long title - - - - - - Current URL is mocked to be: /tab3 - - - Notice how the "Tab 3 With long title" tab is selected (highlighted) - because it matches the current path. - - + + + + + Tab 1 + + + Tab 2 + + + Tab 3 With long title + + + + + + Current URL is mocked to be: /tab3 + + + Notice how the "Tab 3 With long title" tab is selected (highlighted) + because it matches the current path. + + + ), }); @@ -143,32 +150,34 @@ export const WithMockedURLNoMatch = meta.story({ }, render: () => ( - - - - Tab 1 - - - Tab 2 - - - Tab 3 With long title - - - - - - 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 "Tab 1", "Tab 2", "Tab 3 With long title") - fall back to React Aria's internal state. - - + + + + + Tab 1 + + + Tab 2 + + + Tab 3 With long title + + + + + + 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 "Tab 1", "Tab 2", "Tab 3 With long title") + fall back to React Aria's internal state. + + + ), }); @@ -181,32 +190,34 @@ export const ExactMatchingDefault = meta.story({ }, render: () => ( - - - - Mentorship - - - Events - - - Catalog - - - - - - Current URL: /mentorship/events - - - Using default exact matching, only the "Events" tab is active because - it exactly matches the URL. - - - The "Mentorship" tab is NOT active even though the URL contains - "/mentorship". - - + + + + + Mentorship + + + Events + + + Catalog + + + + + + Current URL: /mentorship/events + + + Using default exact matching, only the "Events" tab is active + because it exactly matches the URL. + + + The "Mentorship" tab is NOT active even though the URL contains + "/mentorship". + + + ), }); @@ -217,36 +228,38 @@ export const PrefixMatchingForNestedRoutes = meta.story({ }, render: () => ( - - - - Mentorship - - - Events - - - Catalog - - - - - - Current URL: /mentorship/events - - - The "Mentorship" tab uses prefix matching and IS active because - "/mentorship/events" starts with "/mentorship". - - - The "Events" tab uses exact matching and is also active because it - exactly matches. - - - The "Catalog" tab uses prefix matching but is NOT active because the - URL doesn't start with "/catalog". - - + + + + + Mentorship + + + Events + + + Catalog + + + + + + Current URL: /mentorship/events + + + The "Mentorship" tab uses prefix matching and IS active because + "/mentorship/events" starts with "/mentorship". + + + The "Events" tab uses exact matching and is also active because it + exactly matches. + + + The "Catalog" tab uses prefix matching but is NOT active because the + URL doesn't start with "/catalog". + + + ), }); @@ -257,31 +270,33 @@ export const PrefixMatchingDeepNesting = meta.story({ }, render: () => ( - - - - Home - - - Catalog - - - Mentorship - - - - - - Current URL: /catalog/users/john/details - - - The "Catalog" tab is active because it uses prefix matching and the - URL starts with "/catalog". - - - This works for any level of nesting under "/catalog". - - + + + + + Home + + + Catalog + + + Mentorship + + + + + + Current URL: /catalog/users/john/details + + + The "Catalog" tab is active because it uses prefix matching and the + URL starts with "/catalog". + + + This works for any level of nesting under "/catalog". + + + ), }); @@ -292,47 +307,53 @@ export const MixedMatchingStrategies = meta.story({ }, render: () => ( - - - - Overview - - - Analytics - - - Settings - - - Help - - - - - - Current URL: /dashboard/analytics/reports - - - • "Overview" tab: exact matching, NOT active (doesn't exactly match - "/dashboard") - - - • "Analytics" tab: prefix matching, IS active (URL starts with - "/dashboard/analytics") - - - • "Settings" tab: prefix matching, NOT active (URL doesn't start with - "/dashboard/settings") - - - • "Help" tab: exact matching, NOT active (doesn't exactly match - "/help") - - + + + + + Overview + + + Analytics + + + Settings + + + Help + + + + + + Current URL: /dashboard/analytics/reports + + + • "Overview" tab: exact matching, NOT active (doesn't exactly match + "/dashboard") + + + • "Analytics" tab: prefix matching, IS active (URL starts with + "/dashboard/analytics") + + + • "Settings" tab: prefix matching, NOT active (URL doesn't start + with "/dashboard/settings") + + + • "Help" tab: exact matching, NOT active (doesn't exactly match + "/help") + + + ), }); @@ -343,38 +364,40 @@ export const PrefixMatchingEdgeCases = meta.story({ }, render: () => ( - - - - Foo - - - Foobar - - - Foo (exact) - - - - - - Current URL: /foobar - - - • "Foo" tab (prefix): NOT active - prevents "/foo" from matching - "/foobar" - - - • "Foobar" tab (exact): IS active - exactly matches "/foobar" - - - • "Foo (exact)" tab: NOT active - doesn't exactly match "/foobar" - - - This shows that prefix matching properly requires a "/" separator to - prevent false matches. - - + + + + + Foo + + + Foobar + + + Foo (exact) + + + + + + Current URL: /foobar + + + • "Foo" tab (prefix): NOT active - prevents "/foo" from matching + "/foobar" + + + • "Foobar" tab (exact): IS active - exactly matches "/foobar" + + + • "Foo (exact)" tab: NOT active - doesn't exactly match "/foobar" + + + This shows that prefix matching properly requires a "/" separator to + prevent false matches. + + + ), }); @@ -385,37 +408,39 @@ export const PrefixMatchingWithSlash = meta.story({ }, render: () => ( - - - - Foo - - - Foobar - - - Bar - - - - - - Current URL: /foo/bar - - - • "Foo" tab (prefix): IS active - "/foo/bar" starts with "/foo/" - - - • "Foobar" tab (exact): NOT active - doesn't exactly match "/foobar" - - - • "Bar" tab (prefix): NOT active - "/foo/bar" doesn't start with - "/bar" - - - This demonstrates proper prefix matching with the "/" separator. - - + + + + + Foo + + + Foobar + + + Bar + + + + + + Current URL: /foo/bar + + + • "Foo" tab (prefix): IS active - "/foo/bar" starts with "/foo/" + + + • "Foobar" tab (exact): NOT active - doesn't exactly match "/foobar" + + + • "Bar" tab (prefix): NOT active - "/foo/bar" doesn't start with + "/bar" + + + This demonstrates proper prefix matching with the "/" separator. + + + ), }); @@ -426,32 +451,34 @@ export const RootPathMatching = meta.story({ }, render: () => ( - - - - Home - - - Home (prefix) - - - Catalog - - - - - - Current URL: / - - - • "Home" tab (exact): IS active - exactly matches "/" - - • "Home (prefix)" tab: IS active - "/" matches "/" - - • "Catalog" tab (prefix): NOT active - "/" doesn't start with - "/catalog" - - + + + + + Home + + + Home (prefix) + + + Catalog + + + + + + Current URL: / + + + • "Home" tab (exact): IS active - exactly matches "/" + + • "Home (prefix)" tab: IS active - "/" matches "/" + + • "Catalog" tab (prefix): NOT active - "/" doesn't start with + "/catalog" + + + ), }); @@ -462,41 +489,43 @@ export const HrefWithQueryParams = meta.story({ }, render: () => ( - - - - Dashboard - - - Alerts - - - - - - Current URL: /cost-insights/dashboard?group=bar - - - Tab hrefs include query params (e.g., ?group=foo) but the current URL - has different query params (?group=bar). - - - • "Dashboard" tab: IS active — matching ignores query params and - compares only the pathname. - - - • "Alerts" tab: NOT active — pathname /cost-insights/alerts doesn't - match /cost-insights/dashboard. - - + + + + + Dashboard + + + Alerts + + + + + + Current URL: /cost-insights/dashboard?group=bar + + + Tab hrefs include query params (e.g., ?group=foo) but the current + URL has different query params (?group=bar). + + + • "Dashboard" tab: IS active — matching ignores query params and + compares only the pathname. + + + • "Alerts" tab: NOT active — pathname /cost-insights/alerts doesn't + match /cost-insights/dashboard. + + + ), }); @@ -507,55 +536,57 @@ export const AutoSelectionOfTabs = meta.story({ }, render: () => ( -
- - Current URL: /random-page - + +
+ + Current URL: /random-page + - {/* Without hrefs */} - - {' '} - Case 1: Without hrefs - - - - Settings - Preferences - Advanced - - - Settings content - React Aria manages this selection - - - Preferences content - works normally - - - Advanced content - local state only - - + {/* Without hrefs */} + + {' '} + Case 1: Without hrefs + + + + Settings + Preferences + Advanced + + + Settings content - React Aria manages this selection + + + Preferences content - works normally + + + Advanced content - local state only + + - {/* With hrefs */} - - Case 2: With hrefs - - - By default no selection is shown because the URL doesn't match any - tab's href. - - - - - Catalog - - - Create - - - Docs - - - -
+ {/* With hrefs */} + + Case 2: With hrefs + + + By default no selection is shown because the URL doesn't match any + tab's href. + + + + + Catalog + + + Create + + + Docs + + + +
+
), }); diff --git a/packages/ui/src/components/Tabs/Tabs.tsx b/packages/ui/src/components/Tabs/Tabs.tsx index fcab2c71b9..c489a2ab67 100644 --- a/packages/ui/src/components/Tabs/Tabs.tsx +++ b/packages/ui/src/components/Tabs/Tabs.tsx @@ -50,15 +50,9 @@ import { TabDefinition, TabPanelDefinition, } from './definition'; -import { - isInternalLink, - createRoutingRegistration, -} from '../InternalLinkProvider'; +import { isInternalLink } from '../../utils/linkUtils'; import { getNodeText } from '../../analytics/getNodeText'; -const { RoutingProvider, useRoutingRegistrationEffect } = - createRoutingRegistration(); - const TabsContext = createContext(undefined); const useTabsContext = () => { @@ -218,21 +212,19 @@ export const Tabs = (props: TabsProps) => { ); return ( - - - - - {children as ReactNode} - - - - + + + + {children as ReactNode} + + + ); }; @@ -299,9 +291,6 @@ function RoutedTabEffects({ const selectionCtx = useContext(TabSelectionContext); const location = useLocation(); - // Register with RoutingProvider for conditional RouterProvider wrapping - useRoutingRegistrationEffect(href); - // Register as a routed tab (for controlled vs uncontrolled mode) useEffect(() => { if (selectionCtx) { diff --git a/packages/ui/src/components/TagGroup/TagGroup.stories.tsx b/packages/ui/src/components/TagGroup/TagGroup.stories.tsx index 7d4ee4173e..00c778757f 100644 --- a/packages/ui/src/components/TagGroup/TagGroup.stories.tsx +++ b/packages/ui/src/components/TagGroup/TagGroup.stories.tsx @@ -21,6 +21,7 @@ import type { Selection } from 'react-aria-components'; import { Flex } from '../../'; import { useListData } from 'react-stately'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; import { RiAccountCircleLine, RiBugLine, @@ -50,7 +51,9 @@ const meta = preview.meta({ decorators: [ Story => ( - + + + ), ], diff --git a/packages/ui/src/components/TagGroup/TagGroup.tsx b/packages/ui/src/components/TagGroup/TagGroup.tsx index 8258ab8554..9fd17c6d5c 100644 --- a/packages/ui/src/components/TagGroup/TagGroup.tsx +++ b/packages/ui/src/components/TagGroup/TagGroup.tsx @@ -25,12 +25,8 @@ import { forwardRef, type ReactNode } from 'react'; import { RiCloseCircleLine } from '@remixicon/react'; import { useDefinition } from '../../hooks/useDefinition'; import { TagGroupDefinition, TagDefinition } from './definition'; -import { createRoutingRegistration } from '../InternalLinkProvider'; import { getNodeText } from '../../analytics/getNodeText'; -const { RoutingProvider, useRoutingRegistrationEffect } = - createRoutingRegistration(); - /** * A component that renders a list of tags. * @@ -41,17 +37,15 @@ export const TagGroup = (props: TagGroupProps) => { const { classes, items, children, renderEmptyState } = ownProps; return ( - - - - {children} - - - + + + {children} + + ); }; @@ -68,8 +62,6 @@ export const Tag = forwardRef((props, ref) => { const { classes, children, icon, href } = ownProps; const textValue = typeof children === 'string' ? children : undefined; - useRoutingRegistrationEffect(href); - const handlePress = () => { if (href) { const text = diff --git a/packages/ui/src/guidelines/CardsWithTable.stories.tsx b/packages/ui/src/guidelines/CardsWithTable.stories.tsx index a8ecaf0bef..23e5923327 100644 --- a/packages/ui/src/guidelines/CardsWithTable.stories.tsx +++ b/packages/ui/src/guidelines/CardsWithTable.stories.tsx @@ -18,6 +18,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../provider'; import { Card, CardHeader, @@ -265,7 +266,9 @@ const meta = { decorators: [ (Story: () => JSX.Element) => ( - + + + ), ], diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 2bf478f5fb..8122fd5fde 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -69,11 +69,14 @@ 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, BUIProvider, getNodeText } from './analytics'; +export { useAnalytics, getNodeText } from './analytics'; export type { AnalyticsTracker, AnalyticsEventAttributes, UseAnalyticsFn, - BUIProviderProps, } from './analytics'; diff --git a/packages/ui/src/analytics/BUIProvider.tsx b/packages/ui/src/provider/BUIProvider.tsx similarity index 68% rename from packages/ui/src/analytics/BUIProvider.tsx rename to packages/ui/src/provider/BUIProvider.tsx index 3fcbfc7698..6576a9eabb 100644 --- a/packages/ui/src/analytics/BUIProvider.tsx +++ b/packages/ui/src/provider/BUIProvider.tsx @@ -15,9 +15,11 @@ */ 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 './useAnalytics'; -import type { UseAnalyticsFn } from './types'; +import { BUIContext } from '../analytics/useAnalytics'; +import type { UseAnalyticsFn } from '../analytics/types'; /** @public */ export type BUIProviderProps = { @@ -53,5 +55,23 @@ export function BUIProvider(props: BUIProviderProps) { }), [useAnalytics], ); - return {children}; + + 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/components/InternalLinkProvider/index.ts b/packages/ui/src/provider/index.ts similarity index 68% rename from packages/ui/src/components/InternalLinkProvider/index.ts rename to packages/ui/src/provider/index.ts index d0facb962f..7a99794d47 100644 --- a/packages/ui/src/components/InternalLinkProvider/index.ts +++ b/packages/ui/src/provider/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. @@ -14,11 +14,5 @@ * limitations under the License. */ -export { - InternalLinkProvider, - RoutedContainer, - useRoutingRegistration, - isInternalLink, - createRoutingRegistration, -} from './InternalLinkProvider'; -export type { RoutingContextValue } from './InternalLinkProvider'; +export { BUIProvider } from './BUIProvider'; +export type { BUIProviderProps } from './BUIProvider'; 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/plugins/app/src/extensions/AppRoot.tsx b/plugins/app/src/extensions/AppRoot.tsx index 2e49127a61..9ae564e06d 100644 --- a/plugins/app/src/extensions/AppRoot.tsx +++ b/plugins/app/src/extensions/AppRoot.tsx @@ -124,21 +124,19 @@ export const AppRoot = createExtension({ return [ coreExtensionData.reactElement( - - - el.get(coreExtensionData.reactElement), - )} - > - {content} - - , + + el.get(coreExtensionData.reactElement), + )} + > + {content} + , ), ]; }, @@ -280,23 +278,27 @@ export function AppRouter(props: AppRouterProps) { return ( - {...extraElements} - - {children} + + {...extraElements} + + {children} + ); } return ( - {...extraElements} - - - {children} - + + {...extraElements} + + + {children} + + ); } From 846fb1ea57c292eb895feda23da548a38e225b97 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 19:03:58 +0100 Subject: [PATCH 171/188] Fix TS2454 in createDevApp test Use definite assignment assertions for variables assigned inside jest.isolateModules callbacks. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/createDevApp.test.tsx | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/frontend-dev-utils/src/createDevApp.test.tsx b/packages/frontend-dev-utils/src/createDevApp.test.tsx index b5beb79c1c..6624681152 100644 --- a/packages/frontend-dev-utils/src/createDevApp.test.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.test.tsx @@ -25,6 +25,22 @@ 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 }; @@ -105,11 +121,7 @@ describe('createDevApp', () => { root.id = 'root'; document.body.appendChild(root); - let isolatedCreateDevApp: typeof import('./createDevApp').createDevApp; - jest.isolateModules(() => { - ({ createDevApp: isolatedCreateDevApp } = require('./createDevApp')); - }); - + const isolatedCreateDevApp = loadCreateDevAppIsolated(); isolatedCreateDevApp({ bindRoutes, features: ['plugin-feature'] as any, @@ -160,11 +172,7 @@ describe('createDevApp', () => { root.id = 'root'; document.body.appendChild(root); - let isolatedCreateDevApp: typeof import('./createDevApp').createDevApp; - jest.isolateModules(() => { - ({ createDevApp: isolatedCreateDevApp } = require('./createDevApp')); - }); - + const isolatedCreateDevApp = loadCreateDevAppIsolated(); isolatedCreateDevApp({ features: ['plugin-feature'] as any, }); From 9314ff51625aaa6facb04ff257072b7d03b21a06 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Mon, 16 Mar 2026 18:01:29 +0000 Subject: [PATCH 172/188] Fix Table loading skeleton not showing for complete mode with getData Co-Authored-By: Claude Opus 4.6 Signed-off-by: Jonathan Roebuck --- .changeset/fix-table-complete-mode-loading.md | 5 +++++ .../src/components/Table/hooks/useCompletePagination.ts | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-table-complete-mode-loading.md diff --git a/.changeset/fix-table-complete-mode-loading.md b/.changeset/fix-table-complete-mode-loading.md new file mode 100644 index 0000000000..8e27f2f23f --- /dev/null +++ b/.changeset/fix-table-complete-mode-loading.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Fixed a bug in the `useTable` hook where the loading skeleton was never shown for `complete` mode when using `getData`. The initial data state was an empty array instead of `undefined`, causing the `Table` component to skip the loading state. diff --git a/packages/ui/src/components/Table/hooks/useCompletePagination.ts b/packages/ui/src/components/Table/hooks/useCompletePagination.ts index 5aae9a36c1..d652f5203f 100644 --- a/packages/ui/src/components/Table/hooks/useCompletePagination.ts +++ b/packages/ui/src/components/Table/hooks/useCompletePagination.ts @@ -43,7 +43,7 @@ export function useCompletePagination( const getData = useStableCallback(getDataProp); const { sort, filter, search } = query; - const [items, setItems] = useState([]); + const [items, setItems] = useState(undefined); const [isLoading, setIsLoading] = useState(!data); const [error, setError] = useState(undefined); const [loadCount, setLoadCount] = useState(0); @@ -95,6 +95,9 @@ export function useCompletePagination( // Process data client-side (filter, search, sort) const processedData = useMemo(() => { + if (!resolvedItems) { + return undefined; + } let result = [...resolvedItems]; if (filter !== undefined && filterFn) { result = filterFn(result, filter); @@ -108,11 +111,11 @@ export function useCompletePagination( return result; }, [resolvedItems, sort, filter, search, filterFn, searchFn, sortFn]); - const totalCount = processedData.length; + const totalCount = processedData?.length ?? 0; // Paginate the processed data const paginatedData = useMemo( - () => processedData.slice(offset, offset + pageSize), + () => processedData?.slice(offset, offset + pageSize), [processedData, offset, pageSize], ); From 1c00e2590199bbf571c70c074f8bf1484ed18bab Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 19:09:55 +0100 Subject: [PATCH 173/188] Preserve app loading fallback in createDevApp. Only suspend the lazy BUI CSS import so the app root keeps its built-in loading UI, and lock that behavior down in the mocked render test. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/frontend-dev-utils/src/createDevApp.test.tsx | 5 +++++ packages/frontend-dev-utils/src/createDevApp.tsx | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/frontend-dev-utils/src/createDevApp.test.tsx b/packages/frontend-dev-utils/src/createDevApp.test.tsx index 6624681152..5c0b21ab25 100644 --- a/packages/frontend-dev-utils/src/createDevApp.test.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.test.tsx @@ -134,6 +134,11 @@ describe('createDevApp', () => { }); 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', () => { diff --git a/packages/frontend-dev-utils/src/createDevApp.tsx b/packages/frontend-dev-utils/src/createDevApp.tsx index 112901ed52..80cc95dc22 100644 --- a/packages/frontend-dev-utils/src/createDevApp.tsx +++ b/packages/frontend-dev-utils/src/createDevApp.tsx @@ -108,10 +108,12 @@ export function createDevApp(options: CreateDevAppOptions): void { ReactDOMPromise.then(ReactDOM => { const rootNode = ( - - + <> + + + {AppRoot} - + ); if ('createRoot' in ReactDOM) { From 90a02779fa268dd5543ccc47e261d24a391c6c88 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 19:44:44 +0100 Subject: [PATCH 174/188] Address remaining docs review feedback Clarify the remaining old frontend system sections, tighten the scaffolder follow-up wording, and rename the docs sidebar entry to match the frontend system terminology used elsewhere. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- microsite/sidebars.ts | 6 +++--- plugins/catalog-import/README.md | 6 +++++- plugins/catalog-unprocessed-entities/README.md | 6 +++++- plugins/mui-to-bui/README.md | 6 +++++- plugins/scaffolder/README.md | 9 +++++---- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index cf893d6895..3cffa45747 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -551,15 +551,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/', }, [ diff --git a/plugins/catalog-import/README.md b/plugins/catalog-import/README.md index ba98d19f0b..efb7c84fa3 100644 --- a/plugins/catalog-import/README.md +++ b/plugins/catalog-import/README.md @@ -98,7 +98,11 @@ Following React components accept optional props for providing custom example en ## Old Frontend System -If your Backstage app uses the old frontend system, add the `CatalogImportPage` extension to the app: +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 diff --git a/plugins/catalog-unprocessed-entities/README.md b/plugins/catalog-unprocessed-entities/README.md index 8cdf275851..fc20168b80 100644 --- a/plugins/catalog-unprocessed-entities/README.md +++ b/plugins/catalog-unprocessed-entities/README.md @@ -47,7 +47,11 @@ app: ## Old Frontend System -If your Backstage app uses the old frontend system, import into your `App.tsx` and include into the `` component: +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'; diff --git a/plugins/mui-to-bui/README.md b/plugins/mui-to-bui/README.md index 4217f103a8..0b760c4ef2 100644 --- a/plugins/mui-to-bui/README.md +++ b/plugins/mui-to-bui/README.md @@ -21,7 +21,11 @@ Once installed, the plugin is automatically available in your app through the de ## Old Frontend System -If your Backstage app uses the old frontend system, add a route for the page in your app: +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: ```tsx // packages/app/src/App.tsx diff --git a/plugins/scaffolder/README.md b/plugins/scaffolder/README.md index bc3f0f8ed6..9c9ca15dfa 100644 --- a/plugins/scaffolder/README.md +++ b/plugins/scaffolder/README.md @@ -25,9 +25,9 @@ Once installed, the plugin is automatically available in your app through the de ### Troubleshooting If you encounter [issues with early closure of the `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`. +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 { @@ -74,7 +74,8 @@ to launch the plugin locally using the `createDevApp` of the `./dev/index.tsx` f 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 there, using `yarn start backend` and to specify the locations of the templates to play with! +**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 From 1decc6f9949899caad75519def629eb246403a01 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 20:04:16 +0100 Subject: [PATCH 175/188] Split legacy homepage getting started docs Move the old frontend system homepage guide to its own companion page and add a banner to the main homepage guide that directs legacy users to the split-out documentation. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- docs/getting-started/homepage--old.md | 201 ++++++++++++++++++++++++++ docs/getting-started/homepage.md | 151 +------------------ 2 files changed, 208 insertions(+), 144 deletions(-) create mode 100644 docs/getting-started/homepage--old.md 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 af39a825d8..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. @@ -75,147 +82,3 @@ The home plugin provides powerful customization options: **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). - -## Old Frontend System - -This section covers how to set up the home plugin if your Backstage app still uses the old frontend system. - -### 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 = () => ( - - - - - -); -``` From 872dc1025e4b948200aa8cf440a2daefb3793fbe Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 20:16:25 +0100 Subject: [PATCH 176/188] ui: document table cell wrapper requirement Added TSDoc comments to ColumnConfig.cell, RowRenderFn, CellProps, CellTextProps, and CellProfileProps making it explicit that cell render functions must return a cell component (Cell, CellText, or CellProfile) as the top-level element. Also added a Table Cell Requirement section to the package README with correct and incorrect usage examples. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/document-table-cell-requirement.md | 7 ++ packages/ui/README.md | 28 +++++++ packages/ui/report.api.md | 17 ++-- packages/ui/src/components/Table/types.ts | 82 +++++++++++++++++-- 4 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 .changeset/document-table-cell-requirement.md diff --git a/.changeset/document-table-cell-requirement.md b/.changeset/document-table-cell-requirement.md new file mode 100644 index 0000000000..8a8b82bc90 --- /dev/null +++ b/.changeset/document-table-cell-requirement.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Documented the requirement that `ColumnConfig.cell` and custom `RowRenderFn` renders must return a cell component (`Cell`, `CellText`, or `CellProfile`) as the top-level element. + +Affected components: Table, Cell, CellText, CellProfile diff --git a/packages/ui/README.md b/packages/ui/README.md index 271b78fca4..25de0f3190 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -17,6 +17,34 @@ yarn add @backstage/ui - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) - [Backstage Documentation](https://backstage.io/docs) +## Table Cell Requirement + +When using the `Table` component, 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. + +```tsx +// ✅ 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}; +``` + ## Writing Changesets for Components When creating changesets for component-specific changes, add component metadata to help maintain documentation: diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index b254b4f773..f55efe8c89 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -766,7 +766,7 @@ export const Cell: { displayName: string; }; -// @public (undocumented) +// @public export type CellOwnProps = { className?: string; }; @@ -774,7 +774,7 @@ export type CellOwnProps = { // @public (undocumented) export const CellProfile: (props: CellProfileProps) => JSX_2.Element; -// @public (undocumented) +// @public export type CellProfileOwnProps = { src?: string; name?: string; @@ -784,12 +784,12 @@ export type CellProfileOwnProps = { className?: string; }; -// @public (undocumented) +// @public export interface CellProfileProps extends CellProfileOwnProps, Omit {} -// @public (undocumented) +// @public export interface CellProps extends CellOwnProps, Omit {} @@ -800,7 +800,7 @@ export const CellText: { displayName: string; }; -// @public (undocumented) +// @public export type CellTextOwnProps = { title: string; description?: string; @@ -810,7 +810,7 @@ export type CellTextOwnProps = { className?: string; }; -// @public (undocumented) +// @public export interface CellTextProps extends CellTextOwnProps, Omit {} @@ -849,9 +849,8 @@ export interface CheckboxProps // @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; @@ -2068,7 +2067,7 @@ export interface RowProps extends RowOwnProps, Omit, keyof RowOwnProps> {} -// @public (undocumented) +// @public export type RowRenderFn = (params: { item: T; index: number; diff --git a/packages/ui/src/components/Table/types.ts b/packages/ui/src/components/Table/types.ts index 0736afa05b..030ac44ae0 100644 --- a/packages/ui/src/components/Table/types.ts +++ b/packages/ui/src/components/Table/types.ts @@ -93,17 +93,35 @@ export interface ColumnProps extends ColumnOwnProps, Omit {} -/** @public */ +/** + * Own props for the {@link Cell} component. + * + * @public + */ export type CellOwnProps = { className?: string; }; -/** @public */ +/** + * 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 {} -/** @public */ +/** + * Own props for the {@link CellText} component. + * + * @public + */ export type CellTextOwnProps = { title: string; description?: string; @@ -113,12 +131,25 @@ export type CellTextOwnProps = { className?: string; }; -/** @public */ +/** + * 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 {} -/** @public */ +/** + * Own props for the {@link CellProfile} component. + * + * @public + */ export type CellProfileOwnProps = { src?: string; name?: string; @@ -128,7 +159,16 @@ export type CellProfileOwnProps = { className?: string; }; -/** @public */ +/** + * 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 {} @@ -151,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; @@ -173,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; From 1f25382e569f3eb3f09556f8c1006f7a2a4a3033 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 20:27:02 +0100 Subject: [PATCH 177/188] Move table cell requirement docs to docs-ui Move the cell wrapper requirement documentation from the package README to the docs-ui table component page where it belongs. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/document-table-cell-requirement.md | 7 ----- docs-ui/src/app/components/table/page.mdx | 13 +++++++++ docs-ui/src/app/components/table/snippets.ts | 17 +++++++++++ packages/ui/README.md | 28 ------------------- 4 files changed, 30 insertions(+), 35 deletions(-) delete mode 100644 .changeset/document-table-cell-requirement.md diff --git a/.changeset/document-table-cell-requirement.md b/.changeset/document-table-cell-requirement.md deleted file mode 100644 index 8a8b82bc90..0000000000 --- a/.changeset/document-table-cell-requirement.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Documented the requirement that `ColumnConfig.cell` and custom `RowRenderFn` renders must return a cell component (`Cell`, `CellText`, or `CellProfile`) as the top-level element. - -Affected components: Table, Cell, CellText, CellProfile 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/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/packages/ui/README.md b/packages/ui/README.md index 25de0f3190..271b78fca4 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -17,34 +17,6 @@ yarn add @backstage/ui - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) - [Backstage Documentation](https://backstage.io/docs) -## Table Cell Requirement - -When using the `Table` component, 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. - -```tsx -// ✅ 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}; -``` - ## Writing Changesets for Components When creating changesets for component-specific changes, add component metadata to help maintain documentation: From 0ebde15eef1c91cbb07b727a93f4cb305889ba96 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 20:39:11 +0100 Subject: [PATCH 178/188] Add changeset for table cell documentation Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/document-table-cell-requirement.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/document-table-cell-requirement.md diff --git a/.changeset/document-table-cell-requirement.md b/.changeset/document-table-cell-requirement.md new file mode 100644 index 0000000000..fd99de519f --- /dev/null +++ b/.changeset/document-table-cell-requirement.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Added documentation for the table cell wrapper requirement to TSDoc comments for `Cell`, `CellText`, `CellProfile`, `ColumnConfig`, and `RowRenderFn`. From ed8d9ce67cbd95ccea4ad19386b549f6380c0f36 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 7 Mar 2026 16:36:45 +0100 Subject: [PATCH 179/188] further NFS icon migration and alignment Signed-off-by: Patrik Oldsberg --- .../src/layout/Sidebar/Items.tsx | 15 ++++++++++- .../IconsApi/DefaultIconsApi.test.ts | 27 ++++++++++++++++++- .../IconsApi/DefaultIconsApi.ts | 26 +++++++++++++++++- .../frontend-plugin-api/src/icons/types.ts | 8 +++++- plugins/api-docs/src/alpha.tsx | 4 +-- plugins/app-visualizer/src/plugin.tsx | 2 +- plugins/app/src/extensions/AppNav.tsx | 3 +++ .../extensions/{IconsApi.ts => IconsApi.tsx} | 10 ++++++- .../src/alpha/plugin.tsx | 2 +- plugins/catalog/src/alpha/pages.tsx | 2 +- plugins/catalog/src/alpha/plugin.tsx | 2 +- plugins/devtools/src/alpha/plugin.tsx | 2 +- plugins/scaffolder/src/alpha/plugin.tsx | 2 +- plugins/search/src/alpha.tsx | 2 +- plugins/techdocs/src/alpha/index.tsx | 2 +- plugins/user-settings/src/alpha.tsx | 2 +- 16 files changed, 95 insertions(+), 16 deletions(-) rename plugins/app/src/extensions/{IconsApi.ts => IconsApi.tsx} (85%) 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/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts index cfe6236b95..b739eea09a 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts @@ -59,6 +59,10 @@ describe('DefaultIconsApi', () => { 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'); + // @ts-expect-error accessing internal React element structure + expect(result.props.size).toBe('1em'); }); it('should wrap IconElement values in a component for getIcon()', () => { @@ -69,10 +73,31 @@ describe('DefaultIconsApi', () => { expect(icon).toBeDefined(); expect(typeof icon).toBe('function'); // @ts-expect-error testing runtime behavior - expect(icon({})).toBe(element); + const result = icon({}); + // @ts-expect-error accessing internal React element structure + expect(result.type).toBe('span'); + // @ts-expect-error accessing internal React element structure + expect(result.props.style).toEqual({ + display: 'inline-flex', + fontSize: '1.5rem', + lineHeight: 0, + }); + // @ts-expect-error accessing internal React element structure + expect(result.props.children).toBe(element); 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' }); + // @ts-expect-error accessing internal React element structure + expect(result.props.style.fontSize).toBe('1.25rem'); + }); + it('should wrap null IconElement in a component for getIcon()', () => { const api = new DefaultIconsApi({ empty: null }); 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 cc5ef69b6f..1dfdfbdc9e 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts @@ -21,6 +21,13 @@ import { } from '@backstage/frontend-plugin-api'; import { createElement, isValidElement } from 'react'; +const legacyFontSizeMap = { + inherit: 'inherit', + small: '1.25rem', + medium: '1.5rem', + large: '2.1875rem', +} as const; + /** * Implementation for the {@link IconsApi} * @@ -65,7 +72,24 @@ export class DefaultIconsApi implements IconsApi { if (el === undefined) { return undefined; } - component = () => el; + component = ({ fontSize = 'medium' }) => { + if (el === null) { + return null; + } + + return createElement( + // eslint-disable-next-line react/forbid-elements + 'span', + { + style: { + display: 'inline-flex', + fontSize: legacyFontSizeMap[fontSize], + lineHeight: 0, + }, + }, + el, + ); + }; this.#components.set(key, component); return component; } diff --git a/packages/frontend-plugin-api/src/icons/types.ts b/packages/frontend-plugin-api/src/icons/types.ts index 1a45bcd8fc..68ae60a2f6 100644 --- a/packages/frontend-plugin-api/src/icons/types.ts +++ b/packages/frontend-plugin-api/src/icons/types.ts @@ -38,12 +38,18 @@ export type IconComponent = ComponentType<{ }>; /** - * The type used for icon elements throughout Backstage. + * The type used for icon elements throughout Backstage. It is recommended to + * use icons from `@remixicon/react`. * * @remarks * * Icons should be exactly 24x24 pixels in size. * + * Using icons from `@remixicon/react` is preferred, but using icons from + * `@material-ui/icons` or `AppIcon` and its variants from + * `@backstage/core-components` is supported but depreceated. When using these + * icons, you must set the `fontSize` to `'inherit'`. + * * @public */ export type IconElement = JSX.Element | null; diff --git a/plugins/api-docs/src/alpha.tsx b/plugins/api-docs/src/alpha.tsx index 4afe97dee9..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: () => , }, }); @@ -211,7 +211,7 @@ const apiDocsApisEntityContent = EntityContentBlueprint.make({ export default createFrontendPlugin({ pluginId: 'api-docs', title: 'APIs', - icon: , + icon: , info: { packageJson: () => import('../package.json') }, routes: { root: rootRoute, diff --git a/plugins/app-visualizer/src/plugin.tsx b/plugins/app-visualizer/src/plugin.tsx index 9fa7b2cd72..869f64db39 100644 --- a/plugins/app-visualizer/src/plugin.tsx +++ b/plugins/app-visualizer/src/plugin.tsx @@ -102,7 +102,7 @@ export const visualizerPlugin = createFrontendPlugin({ appVisualizerTreePage, appVisualizerDetailedPage, appVisualizerTextPage, - appVisualizerNavItem, + // appVisualizerNavItem, copyTreeAsJson, ], }); diff --git a/plugins/app/src/extensions/AppNav.tsx b/plugins/app/src/extensions/AppNav.tsx index ffa6ced7b0..129a94cd9e 100644 --- a/plugins/app/src/extensions/AppNav.tsx +++ b/plugins/app/src/extensions/AppNav.tsx @@ -172,6 +172,7 @@ function NavContentRenderer(props: { // 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 && @@ -194,6 +195,8 @@ function NavContentRenderer(props: { icon = ; } else if (resolvedIcon) { icon = resolvedIcon; + } else if (pluginIcon) { + icon = pluginIcon; } if (!title || !icon) { diff --git a/plugins/app/src/extensions/IconsApi.ts b/plugins/app/src/extensions/IconsApi.tsx similarity index 85% rename from plugins/app/src/extensions/IconsApi.ts rename to plugins/app/src/extensions/IconsApi.tsx index d8a97eb7c7..6293c84ed8 100644 --- a/plugins/app/src/extensions/IconsApi.ts +++ b/plugins/app/src/extensions/IconsApi.tsx @@ -45,7 +45,15 @@ export const IconsApi = ApiBlueprint.makeWithOverrides({ return new DefaultIconsApi( inputs.icons .map(i => i.get(IconBundleBlueprint.dataRefs.icons)) - .reduce((acc, bundle) => ({ ...acc, ...bundle }), defaultIcons), + .reduce( + (acc, bundle) => ({ ...acc, ...bundle }), + Object.fromEntries( + Object.entries(defaultIcons).map(([key, Icon]) => [ + key, + , + ]), + ), + ), ); }, }), diff --git a/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx b/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx index 8fd614c886..d2987f3197 100644 --- a/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx +++ b/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx @@ -69,7 +69,7 @@ export const catalogUnprocessedEntitiesNavItem = NavItemBlueprint.make({ export default createFrontendPlugin({ pluginId: 'catalog-unprocessed-entities', title: 'Unprocessed Entities', - icon: , + icon: , info: { packageJson: () => import('../../package.json') }, routes: { root: rootRouteRef, diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index ab71cf39dc..6d64c65b74 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -59,7 +59,7 @@ export const catalogPage = PageBlueprint.makeWithOverrides({ return originalFactory({ path: '/catalog', routeRef: rootRouteRef, - icon: , + icon: , title: 'Catalog', loader: async () => { const { BaseCatalogPage } = await import('../components/CatalogPage'); diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index 8db026016a..243b93db9b 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -40,7 +40,7 @@ import contextMenuItems from './contextMenuItems'; export default createFrontendPlugin({ pluginId: 'catalog', title: 'Catalog', - icon: , + icon: , info: { packageJson: () => import('../../package.json'), }, diff --git a/plugins/devtools/src/alpha/plugin.tsx b/plugins/devtools/src/alpha/plugin.tsx index b1cfa7027f..b991e34bf9 100644 --- a/plugins/devtools/src/alpha/plugin.tsx +++ b/plugins/devtools/src/alpha/plugin.tsx @@ -89,7 +89,7 @@ export const devToolsNavItem = NavItemBlueprint.make({ export default createFrontendPlugin({ pluginId: 'devtools', title: 'DevTools', - icon: , + icon: , info: { packageJson: () => import('../../package.json') }, routes: { root: rootRouteRef, diff --git a/plugins/scaffolder/src/alpha/plugin.tsx b/plugins/scaffolder/src/alpha/plugin.tsx index 374249a804..0c105f3a7c 100644 --- a/plugins/scaffolder/src/alpha/plugin.tsx +++ b/plugins/scaffolder/src/alpha/plugin.tsx @@ -61,7 +61,7 @@ const scaffolderEntityIconLink = EntityIconLinkBlueprint.make({ export default createFrontendPlugin({ pluginId: 'scaffolder', title: 'Create', - icon: , + icon: , info: { packageJson: () => import('../../package.json') }, routes: { root: rootRouteRef, diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index a2ecb61e0e..9625f5e946 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -277,7 +277,7 @@ export const searchNavItem = NavItemBlueprint.make({ export default createFrontendPlugin({ pluginId: 'search', title: 'Search', - icon: , + icon: , info: { packageJson: () => import('../package.json') }, extensions: [searchApi, searchPage, searchNavItem], routes: { diff --git a/plugins/techdocs/src/alpha/index.tsx b/plugins/techdocs/src/alpha/index.tsx index 6acc6b5e6d..1ec92edfd3 100644 --- a/plugins/techdocs/src/alpha/index.tsx +++ b/plugins/techdocs/src/alpha/index.tsx @@ -280,7 +280,7 @@ const techDocsNavItem = NavItemBlueprint.make({ export default createFrontendPlugin({ pluginId: 'techdocs', title: 'Docs', - icon: , + icon: , info: { packageJson: () => import('../../package.json') }, extensions: [ techDocsClientApi, diff --git a/plugins/user-settings/src/alpha.tsx b/plugins/user-settings/src/alpha.tsx index c0342a9b46..87b858fcec 100644 --- a/plugins/user-settings/src/alpha.tsx +++ b/plugins/user-settings/src/alpha.tsx @@ -63,7 +63,7 @@ export const settingsNavItem = NavItemBlueprint.make({ export default createFrontendPlugin({ pluginId: 'user-settings', title: 'Settings', - icon: , + icon: , info: { packageJson: () => import('../package.json') }, extensions: [userSettingsPage, settingsNavItem], routes: { From 3f36ce12572a5d27749f79615375a4886ac1c7aa Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 7 Mar 2026 18:38:02 +0100 Subject: [PATCH 180/188] Clarify icon sizing rules for NFS icons Document the IconElement sizing contract, ensure deprecated icon component registrations inherit size correctly, and add changesets for the affected icon migration packages. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/nfs-icon-alpha-plugins.md | 13 +++++++++++++ .changeset/nfs-icon-foundations.md | 7 +++++++ .../IconsApi/DefaultIconsApi.test.ts | 2 -- .../implementations/IconsApi/DefaultIconsApi.ts | 5 ++++- packages/frontend-plugin-api/src/icons/types.ts | 13 +++++++------ plugins/app/src/extensions/IconsApi.tsx | 10 +--------- 6 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 .changeset/nfs-icon-alpha-plugins.md create mode 100644 .changeset/nfs-icon-foundations.md diff --git a/.changeset/nfs-icon-alpha-plugins.md b/.changeset/nfs-icon-alpha-plugins.md new file mode 100644 index 0000000000..067feb2714 --- /dev/null +++ b/.changeset/nfs-icon-alpha-plugins.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-api-docs': patch +'@backstage/plugin-app-visualizer': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-unprocessed-entities': patch +'@backstage/plugin-devtools': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-search': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-user-settings': patch +--- + +Updated alpha plugin icons to follow the new frontend icon sizing rules when rendered in plugin and navigation surfaces. diff --git a/.changeset/nfs-icon-foundations.md b/.changeset/nfs-icon-foundations.md new file mode 100644 index 0000000000..b36ee47573 --- /dev/null +++ b/.changeset/nfs-icon-foundations.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-components': patch +'@backstage/frontend-app-api': patch +'@backstage/frontend-plugin-api': patch +--- + +Clarified the `IconElement` sizing contract for the new frontend system and aligned legacy system icon rendering with the new icon API. 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 index b739eea09a..87f884d2f2 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts @@ -61,8 +61,6 @@ describe('DefaultIconsApi', () => { expect(result.type).toBe(MyIcon); // @ts-expect-error accessing internal React element structure expect(result.props.fontSize).toBe('inherit'); - // @ts-expect-error accessing internal React element structure - expect(result.props.size).toBe('1em'); }); it('should wrap IconElement values in a component for getIcon()', () => { 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 1dfdfbdc9e..9d36ee72d3 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts @@ -46,7 +46,10 @@ export class DefaultIconsApi implements IconsApi { return [key, value]; } deprecatedKeys.push(key); - return [key, createElement(value as IconComponent)]; + return [ + key, + createElement(value as IconComponent, { fontSize: 'inherit' }), + ]; }), ); diff --git a/packages/frontend-plugin-api/src/icons/types.ts b/packages/frontend-plugin-api/src/icons/types.ts index 68ae60a2f6..180763ad09 100644 --- a/packages/frontend-plugin-api/src/icons/types.ts +++ b/packages/frontend-plugin-api/src/icons/types.ts @@ -38,17 +38,18 @@ export type IconComponent = ComponentType<{ }>; /** - * The type used for icon elements throughout Backstage. It is recommended to - * use icons from `@remixicon/react`. + * The type used for icon elements throughout Backstage. * * @remarks * - * Icons should be exactly 24x24 pixels in size. + * 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, but using icons from + * Using icons from `@remixicon/react` is preferred. Using icons from * `@material-ui/icons` or `AppIcon` and its variants from - * `@backstage/core-components` is supported but depreceated. When using these - * icons, you must set the `fontSize` to `'inherit'`. + * `@backstage/core-components` is supported while migrating, but deprecated. + * When using those icons, you must set `fontSize="inherit"` on the element. * * @public */ diff --git a/plugins/app/src/extensions/IconsApi.tsx b/plugins/app/src/extensions/IconsApi.tsx index 6293c84ed8..d8a97eb7c7 100644 --- a/plugins/app/src/extensions/IconsApi.tsx +++ b/plugins/app/src/extensions/IconsApi.tsx @@ -45,15 +45,7 @@ export const IconsApi = ApiBlueprint.makeWithOverrides({ return new DefaultIconsApi( inputs.icons .map(i => i.get(IconBundleBlueprint.dataRefs.icons)) - .reduce( - (acc, bundle) => ({ ...acc, ...bundle }), - Object.fromEntries( - Object.entries(defaultIcons).map(([key, Icon]) => [ - key, - , - ]), - ), - ), + .reduce((acc, bundle) => ({ ...acc, ...bundle }), defaultIcons), ); }, }), From c0ab3763e553cb58cb6d87accaa5471303019bfb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 14:16:26 +0100 Subject: [PATCH 181/188] Fix tsc errors and add missing changeset Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/nfs-icon-plugin-app.md | 5 +++++ .../IconsApi/DefaultIconsApi.test.ts | 4 ---- plugins/app-visualizer/report.api.md | 22 ------------------- 3 files changed, 5 insertions(+), 26 deletions(-) create mode 100644 .changeset/nfs-icon-plugin-app.md diff --git a/.changeset/nfs-icon-plugin-app.md b/.changeset/nfs-icon-plugin-app.md new file mode 100644 index 0000000000..0f6280a6fb --- /dev/null +++ b/.changeset/nfs-icon-plugin-app.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app': patch +--- + +The app nav now falls back to `plugin.icon` for navigation items that don't have an explicit icon set. 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 index 87f884d2f2..310e9e0184 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts @@ -72,15 +72,12 @@ describe('DefaultIconsApi', () => { expect(typeof icon).toBe('function'); // @ts-expect-error testing runtime behavior const result = icon({}); - // @ts-expect-error accessing internal React element structure expect(result.type).toBe('span'); - // @ts-expect-error accessing internal React element structure expect(result.props.style).toEqual({ display: 'inline-flex', fontSize: '1.5rem', lineHeight: 0, }); - // @ts-expect-error accessing internal React element structure expect(result.props.children).toBe(element); expect(api.getIcon('myIcon')).toBe(icon); }); @@ -92,7 +89,6 @@ describe('DefaultIconsApi', () => { // @ts-expect-error testing runtime behavior const result = icon({ fontSize: 'small' }); - // @ts-expect-error accessing internal React element structure expect(result.props.style.fontSize).toBe('1.25rem'); }); diff --git a/plugins/app-visualizer/report.api.md b/plugins/app-visualizer/report.api.md index 9ee06eccb0..ad8d78c6cb 100644 --- a/plugins/app-visualizer/report.api.md +++ b/plugins/app-visualizer/report.api.md @@ -8,7 +8,6 @@ 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'; @@ -20,27 +19,6 @@ const visualizerPlugin: OverridableFrontendPlugin< {}, {}, { - 'nav-item:app-visualizer': OverridableExtensionDefinition<{ - kind: 'nav-item'; - name: undefined; - config: {}; - configInput: {}; - output: ExtensionDataRef< - { - title: string; - icon: IconComponent; - routeRef: RouteRef; - }, - 'core.nav-item.target', - {} - >; - inputs: {}; - params: { - title: string; - icon: IconComponent; - routeRef: RouteRef; - }; - }>; 'page:app-visualizer': OverridableExtensionDefinition<{ kind: 'page'; name: undefined; From 80fed0e8f0f607ffb4dae9efd9dffc52d87ade65 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 19:42:15 +0100 Subject: [PATCH 182/188] Preserve CSS sizing for translated system icons. Keep the original icon element as the rendered root so legacy MUI-backed icons can still be styled through CSS like other Backstage UI icons. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../IconsApi/DefaultIconsApi.test.ts | 28 ++++++++--- .../IconsApi/DefaultIconsApi.ts | 47 ++++++++++++++----- 2 files changed, 56 insertions(+), 19 deletions(-) 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 index 310e9e0184..558c160ecb 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts @@ -73,12 +73,8 @@ describe('DefaultIconsApi', () => { // @ts-expect-error testing runtime behavior const result = icon({}); expect(result.type).toBe('span'); - expect(result.props.style).toEqual({ - display: 'inline-flex', - fontSize: '1.5rem', - lineHeight: 0, - }); - expect(result.props.children).toBe(element); + expect(result.props.style).toEqual({ fontSize: '1.5rem' }); + expect(result.props.children).toBe(element.props.children); expect(api.getIcon('myIcon')).toBe(icon); }); @@ -92,6 +88,26 @@ describe('DefaultIconsApi', () => { 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 }); 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 9d36ee72d3..52ac07551a 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts @@ -19,7 +19,7 @@ import { IconElement, IconsApi, } from '@backstage/frontend-plugin-api'; -import { createElement, isValidElement } from 'react'; +import { cloneElement, createElement, isValidElement } from 'react'; const legacyFontSizeMap = { inherit: 'inherit', @@ -28,6 +28,14 @@ const legacyFontSizeMap = { 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} * @@ -75,23 +83,36 @@ export class DefaultIconsApi implements IconsApi { if (el === undefined) { return undefined; } - component = ({ fontSize = 'medium' }) => { + component = props => { if (el === null) { return null; } - return createElement( - // eslint-disable-next-line react/forbid-elements - 'span', - { - style: { - display: 'inline-flex', - fontSize: legacyFontSizeMap[fontSize], - lineHeight: 0, - }, + 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, }, - el, - ); + }); }; this.#components.set(key, component); return component; From db69a544194f1760da442f1ae133e34a5f515ae4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 21:37:00 +0100 Subject: [PATCH 183/188] Restore the App Visualizer nav item. Add the App Visualizer navigation item back to the plugin extension list and include the regenerated API report for the restored public surface. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- plugins/app-visualizer/report.api.md | 22 ++++++++++++++++++++++ plugins/app-visualizer/src/plugin.tsx | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/plugins/app-visualizer/report.api.md b/plugins/app-visualizer/report.api.md index ad8d78c6cb..9ee06eccb0 100644 --- a/plugins/app-visualizer/report.api.md +++ b/plugins/app-visualizer/report.api.md @@ -8,6 +8,7 @@ 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'; @@ -19,6 +20,27 @@ const visualizerPlugin: OverridableFrontendPlugin< {}, {}, { + 'nav-item:app-visualizer': OverridableExtensionDefinition<{ + kind: 'nav-item'; + name: undefined; + config: {}; + configInput: {}; + output: ExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >; + inputs: {}; + params: { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }; + }>; 'page:app-visualizer': OverridableExtensionDefinition<{ kind: 'page'; name: undefined; diff --git a/plugins/app-visualizer/src/plugin.tsx b/plugins/app-visualizer/src/plugin.tsx index 869f64db39..9fa7b2cd72 100644 --- a/plugins/app-visualizer/src/plugin.tsx +++ b/plugins/app-visualizer/src/plugin.tsx @@ -102,7 +102,7 @@ export const visualizerPlugin = createFrontendPlugin({ appVisualizerTreePage, appVisualizerDetailedPage, appVisualizerTextPage, - // appVisualizerNavItem, + appVisualizerNavItem, copyTreeAsJson, ], }); From 3b67b7ce6d61a874f46a869a19c2d33d45a20e95 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 21:52:13 +0100 Subject: [PATCH 184/188] Move app-specific config from packages/app into root app-config.yaml The example app (packages/app) had a separate app-config.yaml that was loaded via --config flags in the start script. This moves the NFS configuration (routes, pluginOverrides, extensions) into the root app-config.yaml and removes the --config flags so the app uses the default config resolution. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- app-config.yaml | 124 ++++++++++++++++ packages/app/app-config.yaml | 269 ----------------------------------- packages/app/package.json | 2 +- 3 files changed, 125 insertions(+), 270 deletions(-) delete mode 100644 packages/app/app-config.yaml diff --git a/app-config.yaml b/app-config.yaml index e88955a39f..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 diff --git a/packages/app/app-config.yaml b/packages/app/app-config.yaml deleted file mode 100644 index 37893722f7..0000000000 --- a/packages/app/app-config.yaml +++ /dev/null @@ -1,269 +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 - # 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 - - # 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 add1814f64..22d7436dc6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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": { From 4581c003f934be7427f87efe6c9c708a6536f91d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 21:57:43 +0100 Subject: [PATCH 185/188] Apply suggestions from code review Co-authored-by: Patrik Oldsberg Signed-off-by: Patrik Oldsberg --- .changeset/nfs-icon-alpha-plugins.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/nfs-icon-alpha-plugins.md b/.changeset/nfs-icon-alpha-plugins.md index 067feb2714..42c22bbd6e 100644 --- a/.changeset/nfs-icon-alpha-plugins.md +++ b/.changeset/nfs-icon-alpha-plugins.md @@ -1,6 +1,5 @@ --- '@backstage/plugin-api-docs': patch -'@backstage/plugin-app-visualizer': patch '@backstage/plugin-catalog': patch '@backstage/plugin-catalog-unprocessed-entities': patch '@backstage/plugin-devtools': patch From dee4283ccf363d72ed2057fb6392e635ab52701e Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 17 Mar 2026 08:47:51 +0100 Subject: [PATCH 186/188] feat: add pluginId to actions, server name/description config, dot separator for namespaced tools (#33344) Signed-off-by: benjdlambert --- .changeset/actions-service-plugin-id.md | 7 ++ .changeset/mcp-server-name-description.md | 5 + .../actions/actionsServiceFactory.test.ts | 2 + .../DefaultActionsRegistryService.ts | 1 + .../backend-plugin-api/report-alpha.api.md | 1 + .../src/alpha/ActionsService.ts | 1 + .../src/alpha/services/MockActionsRegistry.ts | 1 + plugins/mcp-actions-backend/README.md | 2 +- plugins/mcp-actions-backend/config.d.ts | 14 ++- .../mcp-actions-backend/src/plugin.test.ts | 8 +- plugins/mcp-actions-backend/src/plugin.ts | 9 ++ .../src/services/McpService.test.ts | 115 +++++++++++++++--- .../src/services/McpService.ts | 9 +- 13 files changed, 150 insertions(+), 25 deletions(-) create mode 100644 .changeset/actions-service-plugin-id.md create mode 100644 .changeset/mcp-server-name-description.md 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/mcp-server-name-description.md b/.changeset/mcp-server-name-description.md new file mode 100644 index 0000000000..55d3629cfa --- /dev/null +++ b/.changeset/mcp-server-name-description.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-mcp-actions-backend': patch +--- + +Added `mcpActions.name` and `mcpActions.description` config options to customize the MCP server identity. Namespaced tool names now use dot separator to align with the MCP spec convention. 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 ffe74f81d9..e0bf4bf145 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts @@ -73,6 +73,7 @@ export class DefaultActionsRegistryService implements ActionsRegistryService { return res.json({ actions: Array.from(this.actions.entries()).map(([id, action]) => ({ id, + pluginId: this.metadata.getId(), ...action, attributes: { // Inspired by the @modelcontextprotocol/sdk defaults for the hints. diff --git a/packages/backend-plugin-api/report-alpha.api.md b/packages/backend-plugin-api/report-alpha.api.md index 7d6786b97e..17b389d9d4 100644 --- a/packages/backend-plugin-api/report-alpha.api.md +++ b/packages/backend-plugin-api/report-alpha.api.md @@ -82,6 +82,7 @@ export interface ActionsService { // @alpha (undocumented) export type ActionsServiceAction = { id: string; + pluginId: string; name: string; title: string; description: string; 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-test-utils/src/alpha/services/MockActionsRegistry.ts b/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts index 384a5e8e95..94d844746f 100644 --- a/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts +++ b/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts @@ -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, diff --git a/plugins/mcp-actions-backend/README.md b/plugins/mcp-actions-backend/README.md index 7793a9f5f9..d496cf0f03 100644 --- a/plugins/mcp-actions-backend/README.md +++ b/plugins/mcp-actions-backend/README.md @@ -73,7 +73,7 @@ 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`. +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: diff --git a/plugins/mcp-actions-backend/config.d.ts b/plugins/mcp-actions-backend/config.d.ts index a535f4c511..a530f5b038 100644 --- a/plugins/mcp-actions-backend/config.d.ts +++ b/plugins/mcp-actions-backend/config.d.ts @@ -16,10 +16,22 @@ 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". + * "get-entity" by the catalog plugin becomes "catalog.get-entity". * Defaults to true. */ namespacedToolNames?: boolean; diff --git a/plugins/mcp-actions-backend/src/plugin.test.ts b/plugins/mcp-actions-backend/src/plugin.test.ts index ceae509f9f..2c03800915 100644 --- a/plugins/mcp-actions-backend/src/plugin.test.ts +++ b/plugins/mcp-actions-backend/src/plugin.test.ts @@ -118,7 +118,7 @@ describe('Mcp Backend', () => { required: ['name'], type: 'object', }, - name: 'local:make-greeting', + name: 'local.make-greeting', }, ]); }); @@ -161,7 +161,7 @@ describe('Mcp Backend', () => { required: ['name'], type: 'object', }, - name: 'local:make-greeting', + name: 'local.make-greeting', }, ]); }); @@ -264,7 +264,7 @@ describe('Mcp Backend', () => { ListToolsResultSchema, ); expect(catalogResult.tools).toHaveLength(1); - expect(catalogResult.tools[0].name).toBe('catalog-actions:get-entity'); + expect(catalogResult.tools[0].name).toBe('catalog-actions.get-entity'); const scaffolderClient = new Client({ name: 'test', version: '1.0' }); const scaffolderTransport = new StreamableHTTPClientTransport( @@ -277,7 +277,7 @@ describe('Mcp Backend', () => { ); expect(scaffolderResult.tools).toHaveLength(1); expect(scaffolderResult.tools[0].name).toBe( - 'scaffolder-actions:create-app', + 'scaffolder-actions.create-app', ); }); }); diff --git a/plugins/mcp-actions-backend/src/plugin.ts b/plugins/mcp-actions-backend/src/plugin.ts index 08f9db350f..f8844249f6 100644 --- a/plugins/mcp-actions-backend/src/plugin.ts +++ b/plugins/mcp-actions-backend/src/plugin.ts @@ -87,9 +87,17 @@ export const mcpPlugin = createBackendPlugin({ 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({ @@ -97,6 +105,7 @@ export const mcpPlugin = createBackendPlugin({ httpAuth, logger, metrics, + serverConfig, }); router.use('/v1/sse', sseRouter); diff --git a/plugins/mcp-actions-backend/src/services/McpService.test.ts b/plugins/mcp-actions-backend/src/services/McpService.test.ts index 8a6656b91c..1a7dc507c3 100644 --- a/plugins/mcp-actions-backend/src/services/McpService.test.ts +++ b/plugins/mcp-actions-backend/src/services/McpService.test.ts @@ -96,7 +96,7 @@ describe('McpService', () => { required: ['input'], type: 'object', }, - name: 'test:mock-action', + name: 'test.mock-action', }, ]); @@ -196,7 +196,7 @@ describe('McpService', () => { const result = await client.request( { method: 'tools/call', - params: { name: 'test:mock-action', arguments: { input: 'test' } }, + params: { name: 'test.mock-action', arguments: { input: 'test' } }, }, CallToolResultSchema, ); @@ -226,7 +226,7 @@ describe('McpService', () => { expect.any(Number), expect.objectContaining({ 'mcp.method.name': 'tools/call', - 'gen_ai.tool.name': 'test:mock-action', + 'gen_ai.tool.name': 'test.mock-action', 'gen_ai.operation.name': 'execute_tool', }), ); @@ -329,7 +329,7 @@ describe('McpService', () => { client.request( { method: 'tools/call', - params: { name: 'test:failing-action', arguments: {} }, + params: { name: 'test.failing-action', arguments: {} }, }, CallToolResultSchema, ), @@ -341,7 +341,7 @@ describe('McpService', () => { expect.any(Number), expect.objectContaining({ 'mcp.method.name': 'tools/call', - 'gen_ai.tool.name': 'test:failing-action', + 'gen_ai.tool.name': 'test.failing-action', 'gen_ai.operation.name': 'execute_tool', 'error.type': 'CustomError', }), @@ -388,7 +388,7 @@ describe('McpService', () => { const result = await client.request( { method: 'tools/call', - params: { name: 'test:failing-action', arguments: { value: 'test' } }, + params: { name: 'test.failing-action', arguments: { value: 'test' } }, }, CallToolResultSchema, ); @@ -444,7 +444,7 @@ describe('McpService', () => { const result = await client.request( { method: 'tools/call', - params: { name: 'test:not-found-action', arguments: { id: 'abc' } }, + params: { name: 'test.not-found-action', arguments: { id: 'abc' } }, }, CallToolResultSchema, ); @@ -464,6 +464,7 @@ describe('McpService', () => { const fakeActions = [ { id: 'catalog:get-entity', + pluginId: 'catalog', name: 'get-entity', title: 'Get Entity', description: 'Fetch an entity', @@ -475,6 +476,7 @@ describe('McpService', () => { }, { id: 'catalog:delete-entity', + pluginId: 'catalog', name: 'delete-entity', title: 'Delete Entity', description: 'Delete an entity', @@ -486,6 +488,7 @@ describe('McpService', () => { }, { id: 'scaffolder:create-app', + pluginId: 'scaffolder', name: 'create-app', title: 'Create App', description: 'Create an app', @@ -571,8 +574,8 @@ describe('McpService', () => { expect(result.tools).toHaveLength(2); expect(result.tools.map(t => t.name)).toEqual([ - 'catalog:get-entity', - 'catalog:delete-entity', + 'catalog.get-entity', + 'catalog.delete-entity', ]); }); @@ -611,7 +614,7 @@ describe('McpService', () => { ); expect(result.tools).toHaveLength(1); - expect(result.tools[0].name).toBe('catalog:get-entity'); + expect(result.tools[0].name).toBe('catalog.get-entity'); }); it('should apply include filter rules with glob patterns', async () => { @@ -649,7 +652,7 @@ describe('McpService', () => { ); expect(result.tools).toHaveLength(1); - expect(result.tools[0].name).toBe('catalog:get-entity'); + expect(result.tools[0].name).toBe('catalog.get-entity'); }); it('should reject tool calls for actions outside the filtered set', async () => { @@ -684,7 +687,7 @@ describe('McpService', () => { const result = await client.request( { method: 'tools/call', - params: { name: 'catalog:get-entity', arguments: {} }, + params: { name: 'catalog.get-entity', arguments: {} }, }, CallToolResultSchema, ); @@ -694,7 +697,7 @@ describe('McpService', () => { { type: 'text', text: expect.stringContaining( - 'Action "catalog:get-entity" not found', + 'Action "catalog.get-entity" not found', ), }, ], @@ -703,6 +706,88 @@ describe('McpService', () => { }); }); + 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(); @@ -739,7 +824,7 @@ describe('McpService', () => { ListToolsResultSchema, ); - expect(result.tools[0].name).toBe('test:mock-action'); + expect(result.tools[0].name).toBe('test.mock-action'); }); it('should use short action name when namespacing is disabled', async () => { @@ -814,7 +899,7 @@ describe('McpService', () => { const result = await client.request( { method: 'tools/call', - params: { name: 'test:mock-action', arguments: {} }, + params: { name: 'test.mock-action', arguments: {} }, }, CallToolResultSchema, ); diff --git a/plugins/mcp-actions-backend/src/services/McpService.ts b/plugins/mcp-actions-backend/src/services/McpService.ts index 18a5e57908..49eea11d72 100644 --- a/plugins/mcp-actions-backend/src/services/McpService.ts +++ b/plugins/mcp-actions-backend/src/services/McpService.ts @@ -76,13 +76,14 @@ export class McpService { credentials: BackstageCredentials; serverConfig?: McpServerConfig; }) { - const serverName = serverConfig?.name ?? 'backstage'; - const server = new McpServer( { - name: serverName, + name: serverConfig?.name ?? 'backstage', // TODO: this version will most likely change in the future. version, + ...(serverConfig?.description && { + description: serverConfig.description, + }), }, { capabilities: { tools: {} } }, ); @@ -222,7 +223,7 @@ export class McpService { private getToolName(action: ActionsServiceAction): string { if (this.namespacedToolNames) { - return action.id; + return `${action.pluginId}.${action.name}`; } return action.name; } From 1b42218ca34b04b76097cc58e59f1ffd4426ff47 Mon Sep 17 00:00:00 2001 From: John Collier Date: Tue, 17 Mar 2026 03:48:59 -0400 Subject: [PATCH 187/188] feat(scaffolder): implement a get scaffolder task log action (#33185) * feat(scaffolder): create get scaffolder task log action Adds a new action to allow retrieving logs from scaffolder tasks, by way of the scaffolderService.getLogs function Signed-off-by: John Collier * Fix typo after rename Signed-off-by: John Collier * Add to list of known actions Signed-off-by: John Collier --------- Signed-off-by: John Collier --- .changeset/gold-squids-rescue.md | 5 + docs/ai/well-known-actions.md | 1 + .../createGetScaffolderTaskLogsAction.test.ts | 148 ++++++++++++++++++ .../createGetScaffolderTaskLogsAction.ts | 112 +++++++++++++ .../scaffolder-backend/src/actions/index.ts | 2 + 5 files changed, 268 insertions(+) create mode 100644 .changeset/gold-squids-rescue.md create mode 100644 plugins/scaffolder-backend/src/actions/createGetScaffolderTaskLogsAction.test.ts create mode 100644 plugins/scaffolder-backend/src/actions/createGetScaffolderTaskLogsAction.ts diff --git a/.changeset/gold-squids-rescue.md b/.changeset/gold-squids-rescue.md new file mode 100644 index 0000000000..8ddc6503f1 --- /dev/null +++ b/.changeset/gold-squids-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Adds a new `get-scaffolder-task-logs` action to `@backstage/plugin-scaffolder-backend` that retrieves log events for a given scaffolder task, with optional support for retrieving only new events after a given event ID. diff --git a/docs/ai/well-known-actions.md b/docs/ai/well-known-actions.md index 14039214d4..d6de8407cf 100644 --- a/docs/ai/well-known-actions.md +++ b/docs/ai/well-known-actions.md @@ -27,3 +27,4 @@ This is a (non-exhaustive) list of actions that are known to be part of the Acti - `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/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/index.ts b/plugins/scaffolder-backend/src/actions/index.ts index 4beb39d832..2dee76d1da 100644 --- a/plugins/scaffolder-backend/src/actions/index.ts +++ b/plugins/scaffolder-backend/src/actions/index.ts @@ -19,6 +19,7 @@ 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; @@ -32,4 +33,5 @@ export const createScaffolderActions = (options: { }); createDryRunTemplateAction(options); createListScaffolderActionsAction(options); + createGetScaffolderTaskLogsAction(options); }; From 05594087b9d9351bf04ce7c6c8a8f997ff2d0485 Mon Sep 17 00:00:00 2001 From: James Brooks <52410024+jabrks@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:31:43 +0000 Subject: [PATCH 188/188] Add virtualized prop to Table component (#33246) Adding a new virtualized prop to the Table component to better support rendering large numbers of rows --- .changeset/lovely-corners-refuse.md | 5 + .../config/vocabularies/Backstage/accept.txt | 1 + packages/ui/report.api.md | 12 ++ .../ui/src/components/Table/Table.module.css | 16 ++ .../src/components/Table/components/Table.tsx | 179 ++++++++++-------- .../ui/src/components/Table/definition.ts | 14 ++ packages/ui/src/components/Table/index.ts | 1 + .../Table/stories/Table.dev.stories.tsx | 150 +++++++++++++++ packages/ui/src/components/Table/types.ts | 7 + 9 files changed, 309 insertions(+), 76 deletions(-) create mode 100644 .changeset/lovely-corners-refuse.md diff --git a/.changeset/lovely-corners-refuse.md b/.changeset/lovely-corners-refuse.md new file mode 100644 index 0000000000..b44698f230 --- /dev/null +++ b/.changeset/lovely-corners-refuse.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Added `virtualized` prop to `Table` component for virtualized rendering of large datasets. Accepts `true` for default row height, `{ rowHeight: number }` for fixed height, or `{ estimatedRowHeight: number }` for variable height rows. diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index fe8306afa8..f3bb31015b 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -555,6 +555,7 @@ validators Valkey varchar viewport +virtualized vite VMware Vodafone diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index f55efe8c89..406ac5316e 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -2522,6 +2522,8 @@ export interface TableProps { sort?: SortState; // (undocumented) style?: React.CSSProperties; + // (undocumented) + virtualized?: VirtualizedProp; } // @public (undocumented) @@ -3045,6 +3047,16 @@ 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; diff --git a/packages/ui/src/components/Table/Table.module.css b/packages/ui/src/components/Table/Table.module.css index a42d86f503..62723db6c8 100644 --- a/packages/ui/src/components/Table/Table.module.css +++ b/packages/ui/src/components/Table/Table.module.css @@ -17,12 +17,28 @@ @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-loading='true'] { diff --git a/packages/ui/src/components/Table/components/Table.tsx b/packages/ui/src/components/Table/components/Table.tsx index 96e7cfe166..2376760b69 100644 --- a/packages/ui/src/components/Table/components/Table.tsx +++ b/packages/ui/src/components/Table/components/Table.tsx @@ -15,7 +15,14 @@ */ import { useId } from 'react-aria'; -import { type Key, ResizableTableContainer } from 'react-aria-components'; +import { + type Key, + ResizableTableContainer, + Virtualizer, +} from 'react-aria-components'; +import { TableLayout } from '@react-stately/layout'; +import { useDefinition } from '../../../hooks/useDefinition'; +import { TableWrapperDefinition } from '../definition'; import { TableRoot } from './TableRoot'; import { TableHeader } from './TableHeader'; import { TableBody } from './TableBody'; @@ -105,7 +112,11 @@ export function Table({ emptyState, className, style, + virtualized, }: TableProps) { + const { + ownProps: { classes }, + } = useDefinition(TableWrapperDefinition, { className }); const liveRegionId = useId(); const visibleColumns = useMemo( @@ -125,7 +136,7 @@ export function Table({ if (error) { return ( -
+
Error: {error.message}
); @@ -148,89 +159,105 @@ export function Table({ const wrapResizable = manualColumnSizing ? (elem: React.ReactNode) => ( - {elem} + + {elem} + ) : (elem: React.ReactNode) => <>{elem}; + const layoutOptions = + typeof virtualized === 'object' ? virtualized : undefined; + + const wrapVirtualized = (elem: React.ReactNode) => + virtualized ? ( + + {elem} + + ) : ( + elem + ); + return ( -
+
{liveRegionLabel} {wrapResizable( - - - {column => - column.header ? ( - column.header() - ) : ( - - {column.label} - - ) - } - - {isInitialLoading ? ( - - ) : ( - {emptyState} : undefined - } - > - {item => { - const itemIndex = data?.indexOf(item) ?? -1; - - if (isRowRenderFn(rowConfig)) { - return rowConfig({ - item, - index: itemIndex, - }); - } - - return ( - rowConfig?.onClick?.(item) - : undefined - } + wrapVirtualized( + + + {column => + column.header ? ( + column.header() + ) : ( + - {column => column.cell(item)} - - ); - }} - - )} - , + {column.label} + + ) + } + + {isInitialLoading ? ( + + ) : ( + {emptyState} : undefined + } + > + {item => { + const itemIndex = data?.indexOf(item) ?? -1; + + if (isRowRenderFn(rowConfig)) { + return rowConfig({ + item, + index: itemIndex, + }); + } + + return ( + rowConfig?.onClick?.(item) + : undefined + } + > + {column => column.cell(item)} + + ); + }} + + )} + , + ), )} {pagination.type === 'page' && ( ()({ + styles, + classNames: { + root: 'bui-TableWrapper', + resizableContainer: 'bui-TableResizableContainer', + }, + propDefs: { + className: {}, + }, +}); + /** * Component definition for Table * @public diff --git a/packages/ui/src/components/Table/index.ts b/packages/ui/src/components/Table/index.ts index 188db635e2..033d51cdaa 100644 --- a/packages/ui/src/components/Table/index.ts +++ b/packages/ui/src/components/Table/index.ts @@ -53,6 +53,7 @@ export type { NoPagination, PagePagination, TablePaginationType, + VirtualizedProp, } from './types'; export type { UseTableOptions, diff --git a/packages/ui/src/components/Table/stories/Table.dev.stories.tsx b/packages/ui/src/components/Table/stories/Table.dev.stories.tsx index 503665ffed..24b8246c4d 100644 --- a/packages/ui/src/components/Table/stories/Table.dev.stories.tsx +++ b/packages/ui/src/components/Table/stories/Table.dev.stories.tsx @@ -798,6 +798,156 @@ export const SelectionReplaceWithRowLinks: Story = { }, }; +export const VirtualizedTable: 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 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/types.ts b/packages/ui/src/components/Table/types.ts index 030ac44ae0..3cfbdff34e 100644 --- a/packages/ui/src/components/Table/types.ts +++ b/packages/ui/src/components/Table/types.ts @@ -253,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[]; @@ -267,4 +273,5 @@ export interface TableProps { emptyState?: ReactNode; className?: string; style?: React.CSSProperties; + virtualized?: VirtualizedProp; }