Merge branch 'master' into package-workspaces

Signed-off-by: Gabriel Dugny <gabriel.dugny@believe.com>

# Conflicts:
#	packages/cli-node/src/pacman/yarn/Yarn.test.ts
#	packages/cli-node/src/pacman/yarn/Yarn.ts
This commit is contained in:
Gabriel Dugny
2026-02-25 09:11:36 +01:00
624 changed files with 10070 additions and 1926 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-search-react': patch
'@backstage/plugin-search': patch
---
Fixes the search component not registering the first search on navigate to the search page.
+28
View File
@@ -0,0 +1,28 @@
---
'@backstage/cli-common': minor
---
Added `targetPaths` and `findOwnPaths` as replacements for `findPaths`, with a cleaner separation between target project paths and package-relative paths.
To migrate existing `findPaths` usage:
```ts
// Before
import { findPaths } from '@backstage/cli-common';
const paths = findPaths(__dirname);
// After — for target project paths (cwd-based):
import { targetPaths } from '@backstage/cli-common';
// paths.targetDir → targetPaths.dir
// paths.targetRoot → targetPaths.rootDir
// paths.resolveTarget('src') → targetPaths.resolve('src')
// paths.resolveTargetRoot('yarn.lock') → targetPaths.resolveRoot('yarn.lock')
// After — for package-relative paths:
import { findOwnPaths } from '@backstage/cli-common';
const own = findOwnPaths(__dirname);
// paths.ownDir → own.dir
// paths.ownRoot → own.rootDir
// paths.resolveOwn('config/jest.js') → own.resolve('config/jest.js')
// paths.resolveOwnRoot('tsconfig.json') → own.resolveRoot('tsconfig.json')
```
+1 -1
View File
@@ -2,4 +2,4 @@
'@backstage/cli': patch
---
Internal refactor to improve module independence.
Internal refactor to use new concurrency utilities from `@backstage/cli-node`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli-node': patch
---
Added `runConcurrentTasks` and `runWorkerQueueThreads` utilities, moved from the `@backstage/cli` internal code.
@@ -0,0 +1,9 @@
---
'@backstage/cli': patch
---
Added `translations export` and `translations import` commands for managing translation files.
The `translations export` command discovers all `TranslationRef` definitions across frontend plugin dependencies and exports their default messages as JSON files. The `translations import` command generates `TranslationResource` wiring code from translated JSON files, ready to be plugged into the app.
Both commands support a `--pattern` option for controlling the message file layout, for example `--pattern '{lang}/{id}.json'` for language-based directory grouping.
@@ -0,0 +1,5 @@
---
'@backstage/repo-tools': patch
---
Fixed prettier existence checks in OpenAPI commands to use `fs.pathExists` instead of checking the resolved path string, which was always truthy.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Fixed focus-visible outline styles for Menu and Select components.
**Affected components:** Menu, Select
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Minor internal optimisation
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/cli': patch
'@backstage/cli-node': patch
'@backstage/backend-dynamic-feature-service': patch
'@backstage/codemods': patch
'@backstage/config-loader': patch
'@backstage/create-app': patch
'@backstage/repo-tools': patch
'@techdocs/cli': patch
---
Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Migrates existing catalog metrics to use the alpha MetricsService. This release is a 1:1 migration with no breaking changes.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog-backend': minor
---
Added opentelemetry metrics for SCM events:
- `catalog.events.scm.messages` with attribute `eventType`: Counter for the number of SCM events actually received by the catalog backend. The `eventType` is currently either `location` or `repository`.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog-node': minor
---
Added the ability for SCM events subscribers to mark the fact that they have taken actions based on events, which produces output metrics:
- `catalog.events.scm.actions` with attribute `action`: Counter for the number of actions actually taken by catalog internals or other subscribers, based on SCM events. The `action` is currently either `create`, `delete`, `refresh`, or `move`.
+39 -1
View File
@@ -210,5 +210,43 @@
"@backstage/plugin-user-settings-backend": "0.4.0",
"@backstage/plugin-user-settings-common": "0.1.0"
},
"changesets": []
"changesets": [
"add-masked-and-hidden-option",
"blue-moons-crash",
"bright-moons-open",
"brown-towns-find",
"bump-bfj-v9",
"cli-common-cached-paths",
"cli-internal-refactor",
"cli-node-parallel-helpers",
"cli-translations-export-import",
"dependabot-d2ec7e9",
"fancy-ends-turn",
"fix-frontend-feature-compat",
"fix-prettier-existence-check",
"fluffy-owls-act",
"long-hairs-throw",
"mean-fans-decide",
"metal-humans-move",
"migrate-to-target-paths",
"ninety-corners-flash",
"orange-mugs-post-1",
"orange-mugs-post-2",
"pink-terms-know",
"polite-singers-lead",
"pretty-days-taste",
"rare-adults-attack",
"renovate-8b1c21e",
"rude-groups-shout",
"scaffolder-export-form-fields-api",
"silver-pigs-remain",
"sixty-pianos-begin",
"stable-translation-plugin-app",
"stable-translation-test-utils",
"stupid-pans-hope",
"swift-flowers-grin",
"swift-ravens-jog",
"tired-bushes-write",
"twenty-worlds-create"
]
}
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/backend-plugin-api': patch
'@backstage/backend-defaults': patch
---
Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/ui': patch
---
Improved type safety in `useDefinition` by centralizing prop resolution and strengthening the `BgPropsConstraint` to require that `bg` provider components declare `children` as a required prop in their OwnProps type.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Added back the `formFieldsApiRef` and `ScaffolderFormFieldsApi` alpha exports that were unintentionally removed.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Added proper cursor styles for RadioGroup items.
**Affected components:** RadioGroup
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/repo-tools': patch
---
Updated `@microsoft/api-extractor` to `7.57.3` and added tests for `getTsDocConfig`
@@ -0,0 +1,5 @@
---
'@backstage/plugin-app': patch
---
Switched translation API imports (`translationApiRef`, `appLanguageApiRef`) from the alpha `@backstage/core-plugin-api/alpha` path to the stable `@backstage/frontend-plugin-api` export. This has no effect on runtime behavior.
@@ -0,0 +1,5 @@
---
'@backstage/frontend-test-utils': patch
---
Switched `MockTranslationApi` and related test utility imports from `@backstage/core-plugin-api/alpha` to the stable `@backstage/frontend-plugin-api` export. The `TranslationApi` type in the API report is now sourced from a single package. This has no effect on runtime behavior.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': patch
---
Minor update to catalog client docs
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-mcp-actions-backend': patch
---
Adds two new metrics to track MCP server operations and sessions.
- `mcp.server.operation.duration`: The duration taken to process an individual MCP operation
- `mcp.server.session.duration`: The duration of the MCP session from the perspective of the server
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Update migrations to be reversible
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
Adds a new metrics service mock to be leveraged in tests
-1
View File
@@ -1 +0,0 @@
Follow the instructions at /.github/copilot-instructions.md
+1
View File
@@ -0,0 +1 @@
../AGENTS.md
-7
View File
@@ -1,7 +0,0 @@
---
description: General project guidelines for Backstage development
globs:
alwaysApply: true
---
Follow the instructions at /.github/copilot-instructions.md
-46
View File
@@ -1,49 +1,3 @@
Backstage is an open platform for building developer portals. This is a TypeScript monorepo using Yarn workspaces.
## Key Directories
- `/packages`: Core framework packages (prefixed `@backstage/`)
- `/plugins`: Plugin packages (prefixed `@backstage/plugin-*`)
- `/packages/app` and `/packages/backend`: Example app for local development
- `/packages/app`: Main example app using the new frontend system
- `/packages/app-legacy`: Example app using the old frontend system
- `/docs`: Documentation files
Packages prefixed with `core-` (e.g., `@backstage/core-plugin-api`) are part of the old frontend system. Packages prefixed with `frontend-` (e.g., `@backstage/frontend-plugin-api`) are part of the new frontend system (NFS). Packages prefixed with `backend-` (e.g., `@backstage/backend-plugin-api`) are part of the backend system.
## Code Standards
The following files contain guidelines for the project:
- `/CONTRIBUTING.md`: comprehensive contribution guidelines.
- `/STYLE.md`: guidelines for code style.
- `/REVIEWING.md`: guidelines for pull requests and writing changesets.
- `/SECURITY.md`: guidelines for security.
- `/docs/architecture-decisions/`: contains the architecture decisions for the project.
## Development Flow
Before any of these commands can be run, you need to run `yarn install` in the project root.
- Build: There is no need to build the project during development, and it is verified automatically in the CI pipeline.
- Test: Use `yarn test --no-watch <path>` in the project root to run tests. The path can be either a single file or a directory. Always provide a path, avoid running all tests.
- Type checking: Use `yarn tsc` in the project root to run the type checker.
- Code formatting: Use `yarn prettier --write <path>` to format code.
- Lint: Use `yarn lint --fix` in the project root to run the linter.
- API reports: Before submitting a pull request with changes to any package in the workspace, run `yarn build:api-reports` in the project root to generate API reports for all packages.
- Dev server: Use `yarn start` to run the example app locally (frontend on :3000, backend on :7007).
- Create: Use `yarn new` to scaffold new plugins, packages, or modules.
You MUST NOT create a release by running `yarn changesets version` or `yarn release` as part of any changes. Releases are created by separate workflows.
All changes that affect the published version of packages in the `/packages` and `/plugins` directories must be accompanied by a changeset. Only non-private packages require changesets. See the guidelines in `/CONTRIBUTING.md#creating-changesets` for information on how to write good changesets. Changesets are stored in the `/.changeset` directory. Breaking changes must be accompanied by a `minor` version bump for packages below version `1.0.0`, or a `major` version bump for packages at version `1.0.0` or higher.
Never update ESLint, Prettier, or TypeScript configuration files unless specifically requested.
## Repository Structure
See `/docs/contribute/project-structure.md` for a detailed description of the repository structure.
## Pull Request Reviews
When reviewing a pull request you MUST calculate a priority score between 0 and 100, where a higher score indicates higher priority. You MUST include the score as an HTML comment at the very beginning of the "PR Overview" section, using the following format: `<!-- priority: <score> -->`.
@@ -322,6 +322,7 @@ openapi
OpenSearch
OpenShift
openssl
opentelemetry
orgs
overridable
padding
-1
View File
@@ -1 +0,0 @@
Add missing sharing extensions sidebar item in frontend system architecture docs
-1
View File
@@ -1 +0,0 @@
Fix type compatibility for older plugins in FrontendFeature type
+1
View File
@@ -0,0 +1 @@
Fixes the search component to register the first search that happens on initial navigation to the search page.
+53
View File
@@ -0,0 +1,53 @@
Backstage is an open platform for building developer portals. This is a TypeScript monorepo using Yarn workspaces.
## Key Directories
- `/packages`: Core framework packages (prefixed `@backstage/`)
- `/plugins`: Plugin packages (prefixed `@backstage/plugin-*`)
- `/packages/app` and `/packages/backend`: Example app for local development
- `/packages/app`: Main example app using the new frontend system
- `/packages/app-legacy`: Example app using the old frontend system
- `/docs`: Documentation files
Packages prefixed with `core-` (e.g., `@backstage/core-plugin-api`) are part of the old frontend system. Packages prefixed with `frontend-` (e.g., `@backstage/frontend-plugin-api`) are part of the new frontend system (NFS). Packages prefixed with `backend-` (e.g., `@backstage/backend-plugin-api`) are part of the backend system.
## Code Standards
The following files contain guidelines for the project:
- `/CONTRIBUTING.md`: comprehensive contribution guidelines.
- `/STYLE.md`: guidelines for code style.
- `/REVIEWING.md`: guidelines for pull requests and writing changesets.
- `/SECURITY.md`: guidelines for security.
- `/docs/architecture-decisions/`: contains the architecture decisions for the project.
When writing or generating code, always match the existing coding style of each individual package and file. Different packages in the monorepo may have different conventions — consistency within a package is more important than consistency across the repo.
When writing or generating tests, prefer fewer thorough tests with multiple assertions over many small tests. When using React Testing Library, prefer using `screen` and `.findBy*` queries over `waitFor`, and avoid adding test IDs to the implementation.
## Development Flow
Before any of these commands can be run, you need to run `yarn install` in the project root.
- Build: There is no need to build the project during development, and it is verified automatically in the CI pipeline.
- Test: Use `CI=1 yarn test <path>` in the project root to run tests. The path can be either a single file or a directory. Always provide a path, avoid running all tests.
- Type checking: Use `yarn tsc` in the project root to run the type checker. Do not try to run it somewhere else than the project root and do not supply any options.
- Code formatting: Use `yarn prettier --write <...paths>` to format code. Run it explicitly for file paths that you know are changed, not for entire folders - otherwise it may change formatting of unrelated files.
- Lint: Use `yarn lint --fix` in the project root to run the linter.
- API reports: Before submitting a pull request with changes to any package in the workspace, run `yarn build:api-reports` in the project root to generate API reports for all packages.
- Dev server: Use `yarn start` to run the example app locally (frontend on :3000, backend on :7007).
- Create: Use `yarn new` to scaffold new plugins, packages, or modules.
You MUST NOT run builds or create a release by running `yarn build`, `yarn changesets version`, or `yarn release` as part of any changes. Builds and releases are made by separate workflows.
All changes that affect the published version of packages in the `/packages` and `/plugins` directories must be accompanied by a changeset. Only non-private packages require changesets. See the guidelines in `/CONTRIBUTING.md#creating-changesets` for information on how to write good changesets. Changesets are stored in the `/.changeset` directory and should be created by writing changeset files directly — never use the changeset CLI. Breaking changes must be accompanied by a `minor` version bump for packages below version `1.0.0`, or a `major` version bump for packages at version `1.0.0` or higher. For non-breaking changes that introduce new APIs or features, use `minor` for packages at version `1.0.0` or higher, and `patch` for packages below `1.0.0`. Each changeset message should be relevant to the specific package it targets and written for Backstage adopters as the audience — avoid referencing internal implementation details. If a change spans multiple packages you often need to create separate changesets to make sure they are tailored to each package.
When creating pull requests, use the template at `/.github/PULL_REQUEST_TEMPLATE.md`.
Never update ESLint, Prettier, or TypeScript configuration files unless specifically requested.
Never make changes to the release notes in `/docs/releases` unless explicitly asked. These document past releases and should not be updated based on newer changes.
## Repository Structure
See `/docs/contribute/project-structure.md` for a detailed description of the repository structure.
+34 -35
View File
@@ -23,7 +23,6 @@ creation-date: 2025-06-23
- [Integration with OpenTelemetry Auto-Instrumentation](#integration-with-opentelemetry-auto-instrumentation)
- [Configuration](#configuration)
- [Interface](#interface)
- [Root Metrics Service](#root-metrics-service)
- [Plugin Metrics Service](#plugin-metrics-service)
- [Example](#example)
- [Release Plan](#release-plan)
@@ -36,7 +35,7 @@ Add a core `MetricsService` to Backstage's framework to provide a unified interf
## Motivation
While individual plugins may implement their own metrics, there's no standardized approach leading to inconsistent metrics patterns across the ecosystem. For example, both `catalog_entities_count` and `catalog.processed.entities.count` are examples of existing metric patterns. Ideally, these would be standardized to `backstage.plugin.catalog.entities.count` and `backstage.plugin.catalog.entities.processed.total` respectively.
While individual plugins may implement their own metrics, there's no standardized approach leading to inconsistent metrics patterns across the ecosystem and incompatibility with OpenTelemetry semantic conventions. For example, a plugin implementing MCP functionality might incorrectly namespace metrics as `backstage_mcp_client_duration` when OpenTelemetry semantic conventions explicitly define `mcp.client.operation.duration` as the standard.
By providing a core metrics service:
@@ -45,7 +44,7 @@ By providing a core metrics service:
### Goals
- Plugin-scoped metric namespacing
- Plugin identification via OpenTelemetry Instrumentation Scope
- Consistent metrics patterns across all plugins
- Aligned with OpenTelemetry industry standards
- Provide a familiar interface as other core services
@@ -124,9 +123,12 @@ The `MetricsService` **complements** rather than duplicates auto-instrumentation
// MetricsService provides (manually):
const entityMetrics = metricsService.createCounter('entities.processed.total');
entityMetrics.add(entities.length, { operation: 'refresh', kind: 'Component' });
entityMetrics.add(entities.length, {
operation: 'refresh',
'entity.kind': 'Component',
});
// Metric is now available as `backstage.plugin.catalog.entities.processed.total`
// Metric is now available as `entities.processed.total`
```
### Configuration
@@ -162,43 +164,21 @@ interface MetricsService {
}
```
#### Root Metrics Service
The `RootMetricsService` is responsible for providing metrics to other root services and creating both plugin-scoped and core-scoped `MetricsService` instances.
```ts
interface RootMetricsService {
// note: no config is provided to the root service.
static forRoot(): RootMetricsService;
forPlugin(pluginId: string): MetricsService;
// final implementation will be similar to
forService(serviceName: string, scope: 'plugin' | 'core'): MetricsService;
}
export const rootMetricsServiceFactory = createServiceFactory({
// depends on as little as possible so that it can be initialized as early as possible.
service: rootMetricsServiceRef,
deps: {},
factory: () => {
return DefaultRootMetricsService.forRoot();
},
});
```
#### Plugin Metrics Service
Each plugin receives a metrics service that automatically namespaces all metrics to match the naming conventions.
Each plugin receives a metrics service that automatically configures the Instrumentation Scope to identify the plugin. The scope name follows the pattern `backstage-plugin-{pluginId}`.
```ts
const metricsServiceFactory = createServiceFactory({
service: metricsServiceRef,
export const metricsServiceFactory = createServiceFactory({
service: coreServices.metrics,
deps: {
rootMetrics: coreServices.rootMetrics,
pluginMetadata: coreServices.pluginMetadata,
},
factory: ({ rootMetrics, pluginMetadata }) => {
return rootMetrics.forPlugin(pluginMetadata.getId());
factory: ({ pluginMetadata }) => {
const pluginId = pluginMetadata.getId();
const scopeName = `backstage-plugin-${pluginId}`;
return new DefaultMetricsService(scopeName, version, ...);
},
});
```
@@ -248,3 +228,22 @@ entitiesProcessed.add(100);
- Plugin authors continue to implement their own metrics as they see fit.
- A combined TelemetryService that provides both metrics and tracing.
### Rejected: Forced Namespace Prefixes
Prepend `backstage.plugin.{pluginId}.` to all metric names. This was the original proposal but conflicts with OpenTelemetry semantic conventions.
**Problems:**
- Makes it impossible to use standard semantic conventions like `mcp.*`, `gen_ai.*`, `http.*`
- Breaks compatibility with industry-standard observability tooling
- Prevents cross-service metric aggregation
- Goes against OpenTelemetry best practices and official guidance
**Example of conflict:**
```ts
// Plugin wants to emit: mcp.client.operation.duration
// Framework forces: backstage.plugin.mcp-actions.mcp.client.operation.duration
// This violates the semantic convention and breaks tooling
```
+164 -16
View File
@@ -1,12 +1,12 @@
---
id: internationalization
title: Internationalization (Experimental)
description: Documentation on adding internationalization to the plugin
title: Internationalization
description: Documentation on adding internationalization to plugins and apps
---
## Overview
The Backstage core function provides internationalization for plugins. The underlying library is [`i18next`](https://www.i18next.com/) with some additional Backstage typescript magic for type safety with keys.
The Backstage core function provides internationalization for plugins and apps. The underlying library is [`i18next`](https://www.i18next.com/) with some additional Backstage typescript magic for type safety with keys.
## For a plugin developer
@@ -183,16 +183,56 @@ return (
The return type of the outer `t` function will be a `JSX.Element`, with the underlying value being a React fragment of the different parts of the message.
## For an application developer overwrite plugin messages
## For an application developer
Step 1: Create translation resources
As an app developer you can both override the default English messages of any plugin, and provide translations for additional languages.
You should separate different translations to their own files and import them in the main file:
### Overriding messages
To customize specific messages without adding new languages, create a translation resource that overrides the default English messages:
```ts
// packages/app/src/translations/catalog.ts
import { createTranslationResource } from '@backstage/frontend-plugin-api';
import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha';
export const catalogTranslations = createTranslationResource({
ref: catalogTranslationRef,
translations: {
en: () =>
Promise.resolve({
default: {
'indexPage.title': 'Service directory',
'indexPage.createButtonTitle': 'Register new service',
},
}),
},
});
```
Then register it in your app:
```diff
+ import { catalogTranslations } from './translations/catalog';
const app = createApp({
+ __experimentalTranslations: {
+ resources: [catalogTranslations],
+ },
})
```
You only need to include the keys you want to override — any missing keys fall back to the plugin's defaults.
### Adding language translations
To add support for additional languages, create translation resources with lazy-loaded message files for each language:
```ts
// packages/app/src/translations/userSettings.ts
import { createTranslationResource } from '@backstage/core-plugin-api/alpha';
import { createTranslationResource } from '@backstage/frontend-plugin-api';
import { userSettingsTranslationRef } from '@backstage/plugin-user-settings/alpha';
export const userSettingsTranslations = createTranslationResource({
@@ -203,10 +243,12 @@ export const userSettingsTranslations = createTranslationResource({
});
```
The translation messages can be defined using `createTranslationMessages` for type safety:
```ts
// packages/app/src/translations/userSettings-zh.ts
import { createTranslationMessages } from '@backstage/core-plugin-api/alpha';
import { createTranslationMessages } from '@backstage/frontend-plugin-api';
import { userSettingsTranslationRef } from '@backstage/plugin-user-settings/alpha';
const zh = createTranslationMessages({
@@ -221,7 +263,7 @@ const zh = createTranslationMessages({
export default zh;
```
It's also possible to export the list of messages directly:
Or as a plain object export:
```ts
// packages/app/src/translations/userSettings-zh.ts
@@ -239,11 +281,7 @@ export default {
};
```
You should change `zh` under the translations object to your local language.
Step 2: Config translations in `packages/app/src/App.tsx`
In an app you can both override the default messages, as well as register translations for additional languages:
Register it with the available languages declared:
```diff
+ import { userSettingsTranslations } from './translations/userSettings';
@@ -256,6 +294,116 @@ In an app you can both override the default messages, as well as register transl
})
```
Step 3: Check everything is working correctly
Go to the Settings page — you should see language switching buttons. Switch languages to verify your translations are loaded correctly.
Go to `Settings` page, you should see change language buttons just under change theme buttons. And then switch language, you should see language had changed
### Using the CLI for full translation workflows
When translating your app to other languages at scale — especially when working with external translation systems — the Backstage CLI provides `translations export` and `translations import` commands that automate the extraction and wiring of translation messages across all your plugin dependencies.
#### Exporting default messages
From your app package directory (e.g. `packages/app`), run:
```bash
yarn backstage-cli translations export
```
This scans all frontend plugin dependencies (including transitive ones) for `TranslationRef` definitions and writes their default English messages as JSON files:
```text
translations/
manifest.json
messages/
catalog.en.json
org.en.json
scaffolder.en.json
...
```
Each `.en.json` file contains the flattened message keys and their default values:
```json
{
"indexPage.title": "All your components",
"indexPage.createButtonTitle": "Create new component",
"entityPage.notFound": "Entity not found"
}
```
#### Creating translations
Copy the exported files and translate them for your target languages:
```bash
cp translations/messages/catalog.en.json translations/messages/catalog.zh.json
```
Then edit `catalog.zh.json` with the translated strings. You only need to include the keys you want to translate — missing keys fall back to the English defaults at runtime.
#### Generating wiring code
Once you have translated files in place, run:
```bash
yarn backstage-cli translations import
```
This generates a TypeScript module at `src/translations/resources.ts` that wires everything together:
```ts
// This file is auto-generated by backstage-cli translations import
// Do not edit manually.
import { createTranslationResource } from '@backstage/frontend-plugin-api';
import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha';
export default [
createTranslationResource({
ref: catalogTranslationRef,
translations: {
zh: () => import('../../translations/messages/catalog.zh.json'),
},
}),
];
```
Import the generated resources in your app:
```ts
import translationResources from './translations/resources';
const app = createApp({
__experimentalTranslations: {
availableLanguages: ['en', 'zh'],
resources: translationResources,
},
});
```
#### Custom file patterns
By default, message files use the pattern `messages/{id}.{lang}.json` (e.g. `messages/catalog.en.json`). You can change this with the `--pattern` option:
```bash
yarn backstage-cli translations export --pattern '{lang}/{id}.json'
```
This produces a directory structure grouped by language instead:
```text
translations/en/catalog.json
translations/zh/catalog.json
```
The pattern is stored in the manifest, so the `import` command automatically uses the same layout.
#### Integration with external translation systems
The exported JSON files are standard key-value pairs compatible with most external translation systems. A typical workflow looks like:
1. Run `translations export` to generate the source English files
2. Upload the `.en.json` files to your translation system
3. Download the translated files back into the translations directory
4. Run `translations import` to regenerate the wiring code
For full command reference, see the [CLI commands documentation](../tooling/cli/03-commands.md#translations-export).
File diff suppressed because it is too large Load Diff
+68
View File
@@ -24,6 +24,7 @@ repo [command] Command that run across an entire
package [command] Lifecycle scripts for individual packages
migrate [command] Migration utilities
versions:bump [options] Bump Backstage packages to the latest versions
translations [command] Translation message management
clean Delete cache directories [DEPRECATED]
build-workspace <workspace-dir> [packages...] Builds a temporary dist workspace from the provided
packages
@@ -429,6 +430,73 @@ YAML file that can be referenced in the GitHub integration configuration.
Usage: backstage-cli create-github-app <github-org>
```
## translations export
Export translation messages from an app and all of its frontend plugins to JSON
files. This command must be run from within a package directory (e.g.
`packages/app`), not from the repository root.
The command discovers all `TranslationRef` definitions in the dependency tree,
extracts their default messages using the TypeScript type system, and writes
them as JSON files along with a manifest.
For more details on the translation workflow, see the
[Internationalization](../../plugins/internationalization.md) documentation.
```text
Usage: backstage-cli translations export [options]
Options:
--output <dir> Output directory for exported messages and manifest (default: "translations")
--pattern <pattern> File path pattern for message files relative to the output
directory, with {id} and {lang} placeholders
(default: "messages/{id}.{lang}.json")
-h, --help display help for command
```
### Examples
Export translations with default settings:
```bash
cd packages/app
yarn backstage-cli translations export
```
Export with language-based directory grouping:
```bash
yarn backstage-cli translations export --pattern '{lang}/{id}.json'
```
## translations import
Generate translation resource wiring code from translated JSON files. Reads the
manifest and translated message files produced by `translations export`, and
generates a TypeScript module that creates `TranslationResource` objects for each
translated ref.
The file pattern used during export is stored in the manifest and automatically
used by the import command.
```text
Usage: backstage-cli translations import [options]
Options:
--input <dir> Input directory containing the manifest and translated message files (default: "translations")
--output <path> Output path for the generated wiring module (default: "src/translations/resources.ts")
-h, --help display help for command
```
### Examples
Generate wiring code with default settings:
```bash
cd packages/app
yarn backstage-cli translations import
```
## info
Outputs debug information which is useful when opening an issue. Outputs system
+1
View File
@@ -142,6 +142,7 @@ nav:
- Composability System: 'plugins/composability.md'
- Plugin Analytics: 'plugins/analytics.md'
- Feature Flags: 'plugins/feature-flags.md'
- Internationalization (i18n): 'plugins/internationalization.md'
- OpenAPI:
- Schema-first plugins with OpenAPI (Experimental): 'openapi/01-getting-started.md'
- Generate a client from your OpenAPI spec: 'openapi/generate-client.md'
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "root",
"version": "1.48.0",
"version": "1.49.0-next.0",
"backstage": {
"cli": {
"new": {
+11
View File
@@ -1,5 +1,16 @@
# @backstage/app-defaults
## 1.7.6-next.0
### Patch Changes
- Updated dependencies
- @backstage/core-app-api@1.19.6-next.0
- @backstage/core-components@0.18.8-next.0
- @backstage/core-plugin-api@1.12.4-next.0
- @backstage/theme@0.7.2
- @backstage/plugin-permission-react@0.4.41-next.0
## 1.7.5
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/app-defaults",
"version": "1.7.5",
"version": "1.7.6-next.0",
"description": "Provides the default wiring of a Backstage App",
"backstage": {
"role": "web-library"
+8
View File
@@ -1,5 +1,13 @@
# app-example-plugin
## 0.0.33-next.0
### Patch Changes
- Updated dependencies
- @backstage/frontend-plugin-api@0.14.2-next.0
- @backstage/core-components@0.18.8-next.0
## 0.0.32
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-example-plugin",
"version": "0.0.32",
"version": "0.0.33-next.0",
"description": "Backstage internal example plugin",
"backstage": {
"role": "frontend-plugin",
+44
View File
@@ -1,5 +1,49 @@
# example-app-legacy
## 0.2.119-next.0
### Patch Changes
- Updated dependencies
- @backstage/ui@0.12.1-next.0
- @backstage/plugin-search-react@1.10.5-next.0
- @backstage/plugin-search@1.6.2-next.0
- @backstage/plugin-api-docs@0.13.5-next.0
- @backstage/cli@0.35.5-next.0
- @backstage/plugin-scaffolder@1.35.5-next.0
- @backstage/plugin-catalog@1.33.1-next.0
- @backstage/plugin-catalog-react@2.0.1-next.0
- @backstage/plugin-mui-to-bui@0.2.5-next.0
- @backstage/plugin-techdocs@1.17.1-next.0
- @backstage/app-defaults@1.7.6-next.0
- @backstage/catalog-model@1.7.6
- @backstage/config@1.3.6
- @backstage/core-app-api@1.19.6-next.0
- @backstage/core-components@0.18.8-next.0
- @backstage/core-plugin-api@1.12.4-next.0
- @backstage/frontend-app-api@0.15.1-next.0
- @backstage/integration-react@1.2.16-next.0
- @backstage/theme@0.7.2
- @backstage/plugin-auth-react@0.1.25-next.0
- @backstage/plugin-catalog-common@1.1.8
- @backstage/plugin-catalog-graph@0.5.8-next.0
- @backstage/plugin-catalog-import@0.13.11-next.0
- @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0
- @backstage/plugin-devtools@0.1.37-next.0
- @backstage/plugin-home@0.9.3-next.0
- @backstage/plugin-home-react@0.1.36-next.0
- @backstage/plugin-kubernetes@0.12.17-next.0
- @backstage/plugin-kubernetes-cluster@0.0.35-next.0
- @backstage/plugin-notifications@0.5.15-next.0
- @backstage/plugin-org@0.6.50-next.0
- @backstage/plugin-permission-react@0.4.41-next.0
- @backstage/plugin-scaffolder-react@1.19.8-next.0
- @backstage/plugin-search-common@1.2.22
- @backstage/plugin-signals@0.0.29-next.0
- @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0
- @backstage/plugin-techdocs-react@1.3.9-next.0
- @backstage/plugin-user-settings@0.9.1-next.0
## 0.2.118
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-app-legacy",
"version": "0.2.118",
"version": "0.2.119-next.0",
"backstage": {
"role": "frontend"
},
+50
View File
@@ -1,5 +1,55 @@
# example-app
## 0.0.33-next.0
### Patch Changes
- Updated dependencies
- @backstage/ui@0.12.1-next.0
- @backstage/plugin-search-react@1.10.5-next.0
- @backstage/plugin-search@1.6.2-next.0
- @backstage/plugin-api-docs@0.13.5-next.0
- @backstage/cli@0.35.5-next.0
- @backstage/frontend-plugin-api@0.14.2-next.0
- @backstage/plugin-scaffolder@1.35.5-next.0
- @backstage/plugin-app@0.4.1-next.0
- @backstage/plugin-app-visualizer@0.2.1-next.0
- @backstage/plugin-catalog@1.33.1-next.0
- @backstage/plugin-catalog-react@2.0.1-next.0
- @backstage/plugin-techdocs@1.17.1-next.0
- @backstage/app-defaults@1.7.6-next.0
- @backstage/catalog-model@1.7.6
- @backstage/config@1.3.6
- @backstage/core-app-api@1.19.6-next.0
- @backstage/core-compat-api@0.5.9-next.0
- @backstage/core-components@0.18.8-next.0
- @backstage/core-plugin-api@1.12.4-next.0
- @backstage/frontend-app-api@0.15.1-next.0
- @backstage/frontend-defaults@0.4.1-next.0
- @backstage/integration-react@1.2.16-next.0
- @backstage/theme@0.7.2
- @backstage/plugin-app-react@0.2.1-next.0
- @backstage/plugin-auth@0.1.6-next.0
- @backstage/plugin-auth-react@0.1.25-next.0
- @backstage/plugin-catalog-common@1.1.8
- @backstage/plugin-catalog-graph@0.5.8-next.0
- @backstage/plugin-catalog-import@0.13.11-next.0
- @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0
- @backstage/plugin-devtools@0.1.37-next.0
- @backstage/plugin-home@0.9.3-next.0
- @backstage/plugin-home-react@0.1.36-next.0
- @backstage/plugin-kubernetes@0.12.17-next.0
- @backstage/plugin-kubernetes-cluster@0.0.35-next.0
- @backstage/plugin-notifications@0.5.15-next.0
- @backstage/plugin-org@0.6.50-next.0
- @backstage/plugin-permission-react@0.4.41-next.0
- @backstage/plugin-scaffolder-react@1.19.8-next.0
- @backstage/plugin-search-common@1.2.22
- @backstage/plugin-signals@0.0.29-next.0
- @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0
- @backstage/plugin-techdocs-react@1.3.9-next.0
- @backstage/plugin-user-settings@0.9.1-next.0
## 0.0.32
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-app",
"version": "0.0.32",
"version": "0.0.33-next.0",
"backstage": {
"role": "frontend"
},
+9
View File
@@ -1,5 +1,14 @@
# @backstage/backend-app-api
## 1.5.1-next.0
### Patch Changes
- Updated dependencies
- @backstage/backend-plugin-api@1.7.1-next.0
- @backstage/config@1.3.6
- @backstage/errors@1.2.7
## 1.5.0
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-app-api",
"version": "1.5.0",
"version": "1.5.1-next.0",
"description": "Core API used by Backstage backend apps",
"backstage": {
"role": "node-library"
+25
View File
@@ -1,5 +1,30 @@
# @backstage/backend-defaults
## 0.15.3-next.0
### Patch Changes
- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1
- d933f62: Add configurable throttling and retry mechanism for GitLab integration.
- b99158a: Fixed `yarn backstage-cli config:check --strict --config app-config.yaml` config validation error by adding
an optional `default` type discriminator to PostgreSQL connection configuration,
allowing `config:check` to properly validate `default` connection configurations.
- 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins.
- Updated dependencies
- @backstage/cli-node@0.2.19-next.0
- @backstage/integration@1.21.0-next.0
- @backstage/config-loader@1.10.9-next.0
- @backstage/backend-plugin-api@1.7.1-next.0
- @backstage/backend-app-api@1.5.1-next.0
- @backstage/backend-dev-utils@0.1.7
- @backstage/config@1.3.6
- @backstage/errors@1.2.7
- @backstage/integration-aws-node@0.1.20
- @backstage/types@1.2.2
- @backstage/plugin-auth-node@0.6.14-next.0
- @backstage/plugin-events-node@0.4.20-next.0
- @backstage/plugin-permission-node@0.10.11-next.0
## 0.15.2
### Patch Changes
+30
View File
@@ -1127,6 +1127,36 @@ export interface Config {
headers?: { [name: string]: string };
};
/**
* Options for the metrics service.
*/
metrics?: {
/**
* Plugin-specific metrics configuration. Each plugin can override meter metadata.
*/
plugin?: {
[pluginId: string]: {
/**
* Meter configuration for this plugin.
*/
meter?: {
/**
* Custom meter name. If not set, defaults to backstage-plugin-{pluginId}.
*/
name?: string;
/**
* Version for the meter.
*/
version?: string;
/**
* Schema URL for the meter.
*/
schemaUrl?: string;
};
};
};
};
/**
* Options to configure the default RootLoggerService.
*/
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-defaults",
"version": "0.15.2",
"version": "0.15.3-next.0",
"description": "Backend defaults used by Backstage backend apps",
"backstage": {
"role": "node-library"
@@ -5,6 +5,7 @@
```ts
import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';
import { ActionsService } from '@backstage/backend-plugin-api/alpha';
import { MetricsService } from '@backstage/backend-plugin-api/alpha';
import { RootSystemMetadataService } from '@backstage/backend-plugin-api/alpha';
import { ServiceFactory } from '@backstage/backend-plugin-api';
@@ -22,6 +23,13 @@ export const actionsServiceFactory: ServiceFactory<
'singleton'
>;
// @alpha
export const metricsServiceFactory: ServiceFactory<
MetricsService,
'plugin',
'singleton'
>;
// @alpha
export const rootSystemMetadataServiceFactory: ServiceFactory<
RootSystemMetadataService,
@@ -38,6 +38,7 @@ import { eventsServiceFactory } from '@backstage/plugin-events-node';
import {
actionsRegistryServiceFactory,
actionsServiceFactory,
metricsServiceFactory,
} from '@backstage/backend-defaults/alpha';
import { instanceMetadataServiceFactory } from './alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory';
@@ -66,6 +67,7 @@ export const defaultServiceFactories = [
// alpha services
actionsRegistryServiceFactory,
actionsServiceFactory,
metricsServiceFactory,
// Unexported alpha services kept around for compatibility reasons
instanceMetadataServiceFactory,
@@ -0,0 +1,117 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { metrics } from '@opentelemetry/api';
import { DefaultMetricsService } from './DefaultMetricsService';
const mockGetMeter = jest.spyOn(metrics, 'getMeter');
describe('DefaultMetricsService', () => {
beforeEach(() => {
mockGetMeter.mockClear();
});
describe('create', () => {
it('should create a MetricsService with name only', () => {
const service = DefaultMetricsService.create({ name: 'test-meter' });
expect(mockGetMeter).toHaveBeenCalledTimes(1);
expect(mockGetMeter).toHaveBeenCalledWith('test-meter', undefined, {
schemaUrl: undefined,
});
expect(service).toBeDefined();
});
it('should create a MetricsService with name, version, and schemaUrl', () => {
const service = DefaultMetricsService.create({
name: 'test-meter',
version: '1.2.3',
schemaUrl: 'https://example.com/schema',
});
expect(mockGetMeter).toHaveBeenCalledTimes(1);
expect(mockGetMeter).toHaveBeenCalledWith('test-meter', '1.2.3', {
schemaUrl: 'https://example.com/schema',
});
expect(service).toBeDefined();
});
});
describe('metric instruments', () => {
it('should create a counter', () => {
const service = DefaultMetricsService.create({ name: 'test' });
const counter = service.createCounter('my_counter', {
description: 'A test counter',
unit: 'bytes',
});
expect(counter).toBeDefined();
expect(counter.add).toBeDefined();
});
it('should create an up-down counter', () => {
const service = DefaultMetricsService.create({ name: 'test' });
const upDownCounter = service.createUpDownCounter('my_updown');
expect(upDownCounter).toBeDefined();
expect(upDownCounter.add).toBeDefined();
});
it('should create a histogram', () => {
const service = DefaultMetricsService.create({ name: 'test' });
const histogram = service.createHistogram('my_histogram');
expect(histogram).toBeDefined();
expect(histogram.record).toBeDefined();
});
it('should create a gauge', () => {
const service = DefaultMetricsService.create({ name: 'test' });
const gauge = service.createGauge('my_gauge');
expect(gauge).toBeDefined();
expect(gauge.record).toBeDefined();
});
it('should create an observable counter', () => {
const service = DefaultMetricsService.create({ name: 'test' });
const counter = service.createObservableCounter('my_observable_counter');
expect(counter).toBeDefined();
expect(counter.addCallback).toBeDefined();
expect(counter.removeCallback).toBeDefined();
});
it('should create an observable up-down counter', () => {
const service = DefaultMetricsService.create({ name: 'test' });
const counter = service.createObservableUpDownCounter(
'my_observable_updown',
);
expect(counter).toBeDefined();
expect(counter.addCallback).toBeDefined();
});
it('should create an observable gauge', () => {
const service = DefaultMetricsService.create({ name: 'test' });
const gauge = service.createObservableGauge('my_observable_gauge');
expect(gauge).toBeDefined();
expect(gauge.addCallback).toBeDefined();
});
});
});
@@ -0,0 +1,123 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Meter, metrics } from '@opentelemetry/api';
import {
MetricsService,
MetricAttributes,
MetricOptions,
MetricsServiceCounter,
MetricsServiceUpDownCounter,
MetricsServiceHistogram,
MetricsServiceGauge,
MetricsServiceObservableCounter,
MetricsServiceObservableGauge,
MetricsServiceObservableUpDownCounter,
} from '@backstage/backend-plugin-api/alpha';
/**
* Options for creating a {@link DefaultMetricsService}.
*
* @alpha
*/
export interface DefaultMetricsServiceOptions {
name: string;
version?: string;
schemaUrl?: string;
}
/**
* Default implementation of the {@link MetricsService} interface.
*
* This implementation provides a thin wrapper around the OpenTelemetry Meter API.
*
* @alpha
*/
export class DefaultMetricsService implements MetricsService {
private readonly meter: Meter;
private constructor(opts: DefaultMetricsServiceOptions) {
// The meter name sets the OpenTelemetry Instrumentation Scope which identifies the source of metrics in telemetry backends.
this.meter = metrics.getMeter(opts.name, opts.version, {
schemaUrl: opts.schemaUrl,
});
}
/**
* Creates a new {@link MetricsService} instance.
*
* @param opts - Options for configuring the meter scope
* @returns A new MetricsService instance
*/
static create(opts: DefaultMetricsServiceOptions): MetricsService {
return new DefaultMetricsService(opts);
}
createCounter<TAttributes extends MetricAttributes = MetricAttributes>(
name: string,
opts?: MetricOptions,
): MetricsServiceCounter<TAttributes> {
return this.meter.createCounter(name, opts);
}
createUpDownCounter<TAttributes extends MetricAttributes = MetricAttributes>(
name: string,
opts?: MetricOptions,
): MetricsServiceUpDownCounter<TAttributes> {
return this.meter.createUpDownCounter(name, opts);
}
createHistogram<TAttributes extends MetricAttributes = MetricAttributes>(
name: string,
opts?: MetricOptions,
): MetricsServiceHistogram<TAttributes> {
return this.meter.createHistogram(name, opts);
}
createGauge<TAttributes extends MetricAttributes = MetricAttributes>(
name: string,
opts?: MetricOptions,
): MetricsServiceGauge<TAttributes> {
return this.meter.createGauge(name, opts);
}
createObservableCounter<
TAttributes extends MetricAttributes = MetricAttributes,
>(
name: string,
opts?: MetricOptions,
): MetricsServiceObservableCounter<TAttributes> {
return this.meter.createObservableCounter(name, opts);
}
createObservableUpDownCounter<
TAttributes extends MetricAttributes = MetricAttributes,
>(
name: string,
opts?: MetricOptions,
): MetricsServiceObservableUpDownCounter<TAttributes> {
return this.meter.createObservableUpDownCounter(name, opts);
}
createObservableGauge<
TAttributes extends MetricAttributes = MetricAttributes,
>(
name: string,
opts?: MetricOptions,
): MetricsServiceObservableGauge<TAttributes> {
return this.meter.createObservableGauge(name, opts);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2020 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.
@@ -13,8 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { findPaths } from '@backstage/cli-common';
/* eslint-disable-next-line no-restricted-syntax */
export const paths = findPaths(__dirname);
export { metricsServiceFactory } from './metricsServiceFactory';
@@ -0,0 +1,130 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
mockServices,
ServiceFactoryTester,
} from '@backstage/backend-test-utils';
import { metricsServiceFactory } from './metricsServiceFactory';
import { DefaultMetricsService } from './DefaultMetricsService';
describe('metricsServiceFactory', () => {
let createSpy: jest.SpyInstance;
beforeEach(() => {
createSpy = jest.spyOn(DefaultMetricsService, 'create');
});
afterEach(() => {
jest.restoreAllMocks();
});
const defaultServices = [
mockServices.rootConfig.factory(),
metricsServiceFactory,
];
it('should use backstage-plugin-{pluginId} as meter name when no config is set', async () => {
await ServiceFactoryTester.from(metricsServiceFactory, {
dependencies: defaultServices,
}).getSubject('my-plugin');
expect(createSpy).toHaveBeenCalledWith({
name: 'backstage-plugin-my-plugin',
version: undefined,
schemaUrl: undefined,
});
});
it('should use custom name from config', async () => {
await ServiceFactoryTester.from(metricsServiceFactory, {
dependencies: [
mockServices.rootConfig.factory({
data: {
backend: {
metrics: {
plugin: {
'my-plugin': {
meter: {
name: 'custom-metrics-name',
},
},
},
},
},
},
}),
metricsServiceFactory,
],
}).getSubject('my-plugin');
expect(createSpy).toHaveBeenCalledWith({
name: 'custom-metrics-name',
version: undefined,
schemaUrl: undefined,
});
});
it('should accept version and schemaUrl from config', async () => {
await ServiceFactoryTester.from(metricsServiceFactory, {
dependencies: [
mockServices.rootConfig.factory({
data: {
backend: {
metrics: {
plugin: {
'my-plugin': {
meter: {
name: 'my-plugin-metrics',
version: '1.2.3',
schemaUrl: 'https://example.com/schema',
},
},
},
},
},
},
}),
metricsServiceFactory,
],
}).getSubject('my-plugin');
expect(createSpy).toHaveBeenCalledWith({
name: 'my-plugin-metrics',
version: '1.2.3',
schemaUrl: 'https://example.com/schema',
});
});
it('should implement the full MetricsService interface', async () => {
const subject = await ServiceFactoryTester.from(metricsServiceFactory, {
dependencies: defaultServices,
}).getSubject('test-plugin');
expect(createSpy).toHaveBeenCalledWith({
name: 'backstage-plugin-test-plugin',
version: undefined,
schemaUrl: undefined,
});
expect(subject.createCounter).toBeDefined();
expect(subject.createUpDownCounter).toBeDefined();
expect(subject.createHistogram).toBeDefined();
expect(subject.createGauge).toBeDefined();
expect(subject.createObservableCounter).toBeDefined();
expect(subject.createObservableUpDownCounter).toBeDefined();
expect(subject.createObservableGauge).toBeDefined();
});
});
@@ -0,0 +1,48 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { metricsServiceRef } from '@backstage/backend-plugin-api/alpha';
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { DefaultMetricsService } from './DefaultMetricsService';
/**
* Service factory for collecting plugin-scoped metrics.
*
* @alpha
*/
export const metricsServiceFactory = createServiceFactory({
service: metricsServiceRef,
deps: {
config: coreServices.rootConfig,
pluginMetadata: coreServices.pluginMetadata,
},
factory: ({ config, pluginMetadata }) => {
const pluginId = pluginMetadata.getId();
const meterConfig = config.getOptionalConfig(
`backend.metrics.plugin.${pluginId}.meter`,
);
const scopeName = `backstage-plugin-${pluginId}`;
const name = meterConfig?.getOptionalString('name') ?? scopeName;
const version = meterConfig?.getOptionalString('version');
const schemaUrl = meterConfig?.getOptionalString('schemaUrl');
return DefaultMetricsService.create({ name, version, schemaUrl });
},
});
@@ -16,4 +16,5 @@
export { actionsRegistryServiceFactory } from './entrypoints/actionsRegistry';
export { actionsServiceFactory } from './entrypoints/actions';
export { metricsServiceFactory } from './entrypoints/metrics';
export { rootSystemMetadataServiceFactory } from './entrypoints/rootSystemMetadata';
@@ -1,5 +1,31 @@
# @backstage/backend-dynamic-feature-service
## 0.7.10-next.0
### Patch Changes
- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`.
- Updated dependencies
- @backstage/cli-common@0.2.0-next.0
- @backstage/cli-node@0.2.19-next.0
- @backstage/backend-defaults@0.15.3-next.0
- @backstage/plugin-catalog-backend@3.5.0-next.0
- @backstage/config-loader@1.10.9-next.0
- @backstage/backend-plugin-api@1.7.1-next.0
- @backstage/backend-openapi-utils@0.6.7-next.0
- @backstage/config@1.3.6
- @backstage/errors@1.2.7
- @backstage/types@1.2.2
- @backstage/plugin-app-node@0.1.43-next.0
- @backstage/plugin-auth-node@0.6.14-next.0
- @backstage/plugin-events-backend@0.5.12-next.0
- @backstage/plugin-events-node@0.4.20-next.0
- @backstage/plugin-permission-common@0.9.6
- @backstage/plugin-permission-node@0.10.11-next.0
- @backstage/plugin-scaffolder-node@0.12.6-next.0
- @backstage/plugin-search-backend-node@1.4.2-next.0
- @backstage/plugin-search-common@1.2.22
## 0.7.9
### Patch Changes
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-dynamic-feature-service",
"version": "0.7.9",
"version": "0.7.10-next.0",
"description": "Backstage dynamic feature service",
"backstage": {
"role": "node-library"
@@ -38,7 +38,7 @@ import { createSpecializedBackend } from '@backstage/backend-app-api';
import { ConfigSources } from '@backstage/config-loader';
import { Logs, MockedLogger, LogContent } from '../__testUtils__/testUtils';
import { PluginScanner } from '../scanner/plugin-scanner';
import { findPaths } from '@backstage/cli-common';
import { targetPaths } from '@backstage/cli-common';
import { createMockDirectory } from '@backstage/backend-test-utils';
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
import { BackstagePackageJson, PackageRole } from '@backstage/cli-node';
@@ -956,7 +956,7 @@ describe('backend-dynamic-feature-service', () => {
mockDir.setContent({
'package.json': fs.readFileSync(
findPaths(__dirname).resolveTargetRoot('package.json'),
targetPaths.resolveRoot('package.json'),
),
'dynamic-plugins-root': {},
'dynamic-plugins-root/a-dynamic-plugin': ctx =>
@@ -1044,7 +1044,7 @@ describe('backend-dynamic-feature-service', () => {
otherMockDir.resolve('a-dynamic-plugin'),
);
expect(mockedModuleLoader.bootstrap).toHaveBeenCalledWith(
findPaths(__dirname).targetRoot,
targetPaths.rootDir,
[realPath],
new Map<string, ScannedPluginManifest>([
[
@@ -35,7 +35,7 @@ import {
createServiceRef,
} from '@backstage/backend-plugin-api';
import { PackageRole, PackageRoles } from '@backstage/cli-node';
import { findPaths } from '@backstage/cli-common';
import { targetPaths } from '@backstage/cli-common';
import * as fs from 'node:fs';
/**
@@ -56,7 +56,7 @@ export class DynamicPluginManager implements DynamicPluginProvider {
options: DynamicPluginManagerOptions,
): Promise<DynamicPluginManager> {
/* eslint-disable-next-line no-restricted-syntax */
const backstageRoot = findPaths(__dirname).targetRoot;
const backstageRoot = targetPaths.rootDir;
const scanner = PluginScanner.create({
config: options.config,
logger: options.logger,
@@ -20,7 +20,7 @@ import {
createServiceFactory,
createServiceRef,
} from '@backstage/backend-plugin-api';
import { findPaths } from '@backstage/cli-common';
import { targetPaths } from '@backstage/cli-common';
import fs from 'fs-extra';
import * as path from 'node:path';
@@ -100,7 +100,7 @@ const dynamicPluginsSchemasServiceFactoryWithOptions = (
config,
logger,
// eslint-disable-next-line no-restricted-syntax
backstageRoot: findPaths(__dirname).targetRoot,
backstageRoot: targetPaths.rootDir,
preferAlpha: true,
});
@@ -1,5 +1,14 @@
# @backstage/backend-openapi-utils
## 0.6.7-next.0
### Patch Changes
- Updated dependencies
- @backstage/backend-plugin-api@1.7.1-next.0
- @backstage/errors@1.2.7
- @backstage/types@1.2.2
## 0.6.6
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-openapi-utils",
"version": "0.6.6",
"version": "0.6.7-next.0",
"description": "OpenAPI typescript support.",
"backstage": {
"role": "node-library"
+14
View File
@@ -1,5 +1,19 @@
# @backstage/backend-plugin-api
## 1.7.1-next.0
### Patch Changes
- 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins.
- Updated dependencies
- @backstage/cli-common@0.2.0-next.0
- @backstage/config@1.3.6
- @backstage/errors@1.2.7
- @backstage/types@1.2.2
- @backstage/plugin-auth-node@0.6.14-next.0
- @backstage/plugin-permission-common@0.9.6
- @backstage/plugin-permission-node@0.10.11-next.0
## 1.7.0
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-plugin-api",
"version": "1.7.0",
"version": "1.7.1-next.0",
"description": "Core API used by Backstage backend plugins",
"backstage": {
"role": "node-library"
@@ -103,6 +103,150 @@ export const actionsServiceRef: ServiceRef<
'singleton'
>;
// @alpha
export interface MetricAdvice {
explicitBucketBoundaries?: number[];
}
// @alpha
export interface MetricAttributes {
// (undocumented)
[attributeKey: string]: MetricAttributeValue | undefined;
}
// @alpha
export type MetricAttributeValue =
| string
| number
| boolean
| Array<null | undefined | string>
| Array<null | undefined | number>
| Array<null | undefined | boolean>;
// @alpha
export interface MetricOptions {
advice?: MetricAdvice;
description?: string;
unit?: string;
}
// @alpha
export interface MetricsService {
createCounter<TAttributes extends MetricAttributes = MetricAttributes>(
name: string,
opts?: MetricOptions,
): MetricsServiceCounter<TAttributes>;
createGauge<TAttributes extends MetricAttributes = MetricAttributes>(
name: string,
opts?: MetricOptions,
): MetricsServiceGauge<TAttributes>;
createHistogram<TAttributes extends MetricAttributes = MetricAttributes>(
name: string,
opts?: MetricOptions,
): MetricsServiceHistogram<TAttributes>;
createObservableCounter<
TAttributes extends MetricAttributes = MetricAttributes,
>(
name: string,
opts?: MetricOptions,
): MetricsServiceObservableCounter<TAttributes>;
createObservableGauge<
TAttributes extends MetricAttributes = MetricAttributes,
>(
name: string,
opts?: MetricOptions,
): MetricsServiceObservableGauge<TAttributes>;
createObservableUpDownCounter<
TAttributes extends MetricAttributes = MetricAttributes,
>(
name: string,
opts?: MetricOptions,
): MetricsServiceObservableUpDownCounter<TAttributes>;
createUpDownCounter<TAttributes extends MetricAttributes = MetricAttributes>(
name: string,
opts?: MetricOptions,
): MetricsServiceUpDownCounter<TAttributes>;
}
// @alpha
export interface MetricsServiceCounter<
TAttributes extends MetricAttributes = MetricAttributes,
> {
// (undocumented)
add(value: number, attributes?: TAttributes): void;
}
// @alpha
export interface MetricsServiceGauge<
TAttributes extends MetricAttributes = MetricAttributes,
> {
// (undocumented)
record(value: number, attributes?: TAttributes): void;
}
// @alpha
export interface MetricsServiceHistogram<
TAttributes extends MetricAttributes = MetricAttributes,
> {
// (undocumented)
record(value: number, attributes?: TAttributes): void;
}
// @alpha
export interface MetricsServiceObservable<
TAttributes extends MetricAttributes = MetricAttributes,
> {
// (undocumented)
addCallback(callback: MetricsServiceObservableCallback<TAttributes>): void;
// (undocumented)
removeCallback(callback: MetricsServiceObservableCallback<TAttributes>): void;
}
// @alpha
export type MetricsServiceObservableCallback<
TAttributes extends MetricAttributes = MetricAttributes,
> = (
observableResult: MetricsServiceObservableResult<TAttributes>,
) => void | Promise<void>;
// @alpha
export type MetricsServiceObservableCounter<
TAttributes extends MetricAttributes = MetricAttributes,
> = MetricsServiceObservable<TAttributes>;
// @alpha
export type MetricsServiceObservableGauge<
TAttributes extends MetricAttributes = MetricAttributes,
> = MetricsServiceObservable<TAttributes>;
// @alpha
export interface MetricsServiceObservableResult<
TAttributes extends MetricAttributes = MetricAttributes,
> {
// (undocumented)
observe(value: number, attributes?: TAttributes): void;
}
// @alpha
export type MetricsServiceObservableUpDownCounter<
TAttributes extends MetricAttributes = MetricAttributes,
> = MetricsServiceObservable<TAttributes>;
// @alpha
export const metricsServiceRef: ServiceRef<
MetricsService,
'plugin',
'singleton'
>;
// @alpha
export interface MetricsServiceUpDownCounter<
TAttributes extends MetricAttributes = MetricAttributes,
> {
// (undocumented)
add(value: number, attributes?: TAttributes): void;
}
// @public (undocumented)
export interface RootSystemMetadataService {
// (undocumented)
@@ -0,0 +1,273 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Attribute values that can be attached to metric measurements.
*
* @alpha
*/
export type MetricAttributeValue =
| string
| number
| boolean
| Array<null | undefined | string>
| Array<null | undefined | number>
| Array<null | undefined | boolean>;
/**
* A set of key-value pairs that can be attached to metric measurements.
*
* @alpha
*/
export interface MetricAttributes {
[attributeKey: string]: MetricAttributeValue | undefined;
}
/**
* Advisory options that influence aggregation configuration.
*
* @alpha
*/
export interface MetricAdvice {
/**
* Hint the explicit bucket boundaries for histogram aggregation.
*/
explicitBucketBoundaries?: number[];
}
/**
* Options for creating a metric instrument.
*
* @alpha
*/
export interface MetricOptions {
/**
* The description of the Metric.
*/
description?: string;
/**
* The unit of the Metric values.
*/
unit?: string;
/**
* Advisory options that influence aggregation configuration.
*/
advice?: MetricAdvice;
}
/**
* A counter metric that only supports non-negative increments.
*
* @alpha
*/
export interface MetricsServiceCounter<
TAttributes extends MetricAttributes = MetricAttributes,
> {
add(value: number, attributes?: TAttributes): void;
}
/**
* A counter metric that supports both positive and negative increments.
*
* @alpha
*/
export interface MetricsServiceUpDownCounter<
TAttributes extends MetricAttributes = MetricAttributes,
> {
add(value: number, attributes?: TAttributes): void;
}
/**
* A histogram metric for recording distributions of values.
*
* @alpha
*/
export interface MetricsServiceHistogram<
TAttributes extends MetricAttributes = MetricAttributes,
> {
record(value: number, attributes?: TAttributes): void;
}
/**
* A gauge metric for recording instantaneous values.
*
* @alpha
*/
export interface MetricsServiceGauge<
TAttributes extends MetricAttributes = MetricAttributes,
> {
record(value: number, attributes?: TAttributes): void;
}
/**
* The result object passed to observable metric callbacks.
*
* @alpha
*/
export interface MetricsServiceObservableResult<
TAttributes extends MetricAttributes = MetricAttributes,
> {
observe(value: number, attributes?: TAttributes): void;
}
/**
* A callback function for observable metrics. Called whenever a metric
* collection is initiated.
*
* @alpha
*/
export type MetricsServiceObservableCallback<
TAttributes extends MetricAttributes = MetricAttributes,
> = (
observableResult: MetricsServiceObservableResult<TAttributes>,
) => void | Promise<void>;
/**
* An observable metric instrument that reports values via callbacks.
*
* @alpha
*/
export interface MetricsServiceObservable<
TAttributes extends MetricAttributes = MetricAttributes,
> {
addCallback(callback: MetricsServiceObservableCallback<TAttributes>): void;
removeCallback(callback: MetricsServiceObservableCallback<TAttributes>): void;
}
/**
* An observable counter metric that reports non-negative sums via callbacks.
*
* @alpha
*/
export type MetricsServiceObservableCounter<
TAttributes extends MetricAttributes = MetricAttributes,
> = MetricsServiceObservable<TAttributes>;
/**
* An observable counter metric that reports sums that can go up or down
* via callbacks.
*
* @alpha
*/
export type MetricsServiceObservableUpDownCounter<
TAttributes extends MetricAttributes = MetricAttributes,
> = MetricsServiceObservable<TAttributes>;
/**
* An observable gauge metric that reports instantaneous values via callbacks.
*
* @alpha
*/
export type MetricsServiceObservableGauge<
TAttributes extends MetricAttributes = MetricAttributes,
> = MetricsServiceObservable<TAttributes>;
/**
* A service that provides a facility for emitting metrics.
*
* @alpha
*/
export interface MetricsService {
/**
* Creates a new counter metric.
*
* @param name - The name of the metric.
* @param opts - The options for the metric.
* @returns The counter metric.
*/
createCounter<TAttributes extends MetricAttributes = MetricAttributes>(
name: string,
opts?: MetricOptions,
): MetricsServiceCounter<TAttributes>;
/**
* Creates a new up-down counter metric.
*
* @param name - The name of the metric.
* @param opts - The options for the metric.
* @returns The up-down counter metric.
*/
createUpDownCounter<TAttributes extends MetricAttributes = MetricAttributes>(
name: string,
opts?: MetricOptions,
): MetricsServiceUpDownCounter<TAttributes>;
/**
* Creates a new histogram metric.
*
* @param name - The name of the metric.
* @param opts - The options for the metric.
* @returns The histogram metric.
*/
createHistogram<TAttributes extends MetricAttributes = MetricAttributes>(
name: string,
opts?: MetricOptions,
): MetricsServiceHistogram<TAttributes>;
/**
* Creates a new gauge metric.
*
* @param name - The name of the metric.
* @param opts - The options for the metric.
* @returns The gauge metric.
*/
createGauge<TAttributes extends MetricAttributes = MetricAttributes>(
name: string,
opts?: MetricOptions,
): MetricsServiceGauge<TAttributes>;
/**
* Creates a new observable counter metric.
*
* @param name - The name of the metric.
* @param opts - The options for the metric.
* @returns The observable counter metric.
*/
createObservableCounter<
TAttributes extends MetricAttributes = MetricAttributes,
>(
name: string,
opts?: MetricOptions,
): MetricsServiceObservableCounter<TAttributes>;
/**
* Creates a new observable up-down counter metric.
*
* @param name - The name of the metric.
* @param opts - The options for the metric.
* @returns The observable up-down counter metric.
*/
createObservableUpDownCounter<
TAttributes extends MetricAttributes = MetricAttributes,
>(
name: string,
opts?: MetricOptions,
): MetricsServiceObservableUpDownCounter<TAttributes>;
/**
* Creates a new observable gauge metric.
*
* @param name - The name of the metric.
* @param opts - The options for the metric.
* @returns The observable gauge metric.
*/
createObservableGauge<
TAttributes extends MetricAttributes = MetricAttributes,
>(
name: string,
opts?: MetricOptions,
): MetricsServiceObservableGauge<TAttributes>;
}
@@ -27,8 +27,27 @@ export type {
export type { ActionsService, ActionsServiceAction } from './ActionsService';
export type {
MetricsService,
MetricAdvice,
MetricAttributes,
MetricAttributeValue,
MetricOptions,
MetricsServiceCounter,
MetricsServiceUpDownCounter,
MetricsServiceHistogram,
MetricsServiceGauge,
MetricsServiceObservable,
MetricsServiceObservableCallback,
MetricsServiceObservableCounter,
MetricsServiceObservableGauge,
MetricsServiceObservableResult,
MetricsServiceObservableUpDownCounter,
} from './MetricsService';
export {
actionsRegistryServiceRef,
actionsServiceRef,
metricsServiceRef,
rootSystemMetadataServiceRef,
} from './refs';
@@ -56,3 +56,14 @@ export const rootSystemMetadataServiceRef = createServiceRef<
id: 'alpha.core.rootSystemMetadata',
scope: 'root',
});
/**
* Service for managing metrics.
*
* @alpha
*/
export const metricsServiceRef = createServiceRef<
import('./MetricsService').MetricsService
>({
id: 'alpha.core.metrics',
});
+16
View File
@@ -1,5 +1,21 @@
# @backstage/backend-test-utils
## 1.11.1-next.0
### Patch Changes
- 1ee5b28: Adds a new metrics service mock to be leveraged in tests
- Updated dependencies
- @backstage/backend-defaults@0.15.3-next.0
- @backstage/backend-plugin-api@1.7.1-next.0
- @backstage/backend-app-api@1.5.1-next.0
- @backstage/config@1.3.6
- @backstage/errors@1.2.7
- @backstage/types@1.2.2
- @backstage/plugin-auth-node@0.6.14-next.0
- @backstage/plugin-events-node@0.4.20-next.0
- @backstage/plugin-permission-common@0.9.6
## 1.11.0
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-test-utils",
"version": "1.11.0",
"version": "1.11.1-next.0",
"description": "Test helpers library for Backstage backends",
"backstage": {
"role": "node-library"
@@ -12,6 +12,7 @@ import { BackstageCredentials } from '@backstage/backend-plugin-api';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { LoggerService } from '@backstage/backend-plugin-api';
import { MetricsService } from '@backstage/backend-plugin-api/alpha';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @alpha (undocumented)
@@ -43,6 +44,16 @@ export namespace actionsServiceMock {
) => ServiceMock<ActionsService>;
}
// @alpha (undocumented)
export namespace metricsServiceMock {
const // (undocumented)
factory: () => ServiceFactory<MetricsService, 'plugin', 'singleton'>;
const // (undocumented)
mock: (
partialImpl?: Partial<MetricsService> | undefined,
) => ServiceMock<MetricsService>;
}
// @alpha
export class MockActionsRegistry
implements ActionsRegistryService, ActionsService
@@ -0,0 +1,59 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createServiceMock } from './alphaCreateServiceMock';
import {
MetricsService,
metricsServiceRef,
} from '@backstage/backend-plugin-api/alpha';
import { metricsServiceFactory } from '@backstage/backend-defaults/alpha';
/**
* @alpha
*/
export namespace metricsServiceMock {
export const factory = () => metricsServiceFactory;
export const mock = createServiceMock<MetricsService>(
metricsServiceRef,
() => ({
createCounter: jest.fn().mockImplementation(() => ({
add: jest.fn(),
})),
createUpDownCounter: jest.fn().mockImplementation(() => ({
add: jest.fn(),
})),
createHistogram: jest.fn().mockImplementation(() => ({
record: jest.fn(),
})),
createGauge: jest.fn().mockImplementation(() => ({
record: jest.fn(),
})),
createObservableCounter: jest.fn().mockImplementation(() => ({
addCallback: jest.fn(),
removeCallback: jest.fn(),
})),
createObservableUpDownCounter: jest.fn().mockImplementation(() => ({
addCallback: jest.fn(),
removeCallback: jest.fn(),
})),
createObservableGauge: jest.fn().mockImplementation(() => ({
addCallback: jest.fn(),
removeCallback: jest.fn(),
})),
}),
);
}
@@ -17,4 +17,5 @@
export { actionsRegistryServiceMock } from './ActionsRegistryServiceMock';
export { MockActionsRegistry } from './MockActionsRegistry';
export { actionsServiceMock } from './ActionsServiceMock';
export { metricsServiceMock } from './MetricsServiceMock';
export { type ServiceMock } from './alphaCreateServiceMock';
@@ -43,6 +43,7 @@ import { HostDiscovery } from '@backstage/backend-defaults/discovery';
import {
actionsRegistryServiceMock,
actionsServiceMock,
metricsServiceMock,
} from '../alpha/services';
/** @public */
@@ -92,6 +93,7 @@ export const defaultServiceFactories = [
// Alpha services
actionsRegistryServiceMock.factory(),
actionsServiceMock.factory(),
metricsServiceMock.factory(),
];
/**
+42
View File
@@ -1,5 +1,47 @@
# example-backend
## 0.0.48-next.0
### Patch Changes
- Updated dependencies
- @backstage/backend-defaults@0.15.3-next.0
- @backstage/plugin-auth-backend@0.27.1-next.0
- @backstage/plugin-catalog-backend@3.5.0-next.0
- @backstage/plugin-scaffolder-backend@3.1.4-next.0
- @backstage/backend-plugin-api@1.7.1-next.0
- @backstage/plugin-mcp-actions-backend@0.1.10-next.0
- @backstage/catalog-model@1.7.6
- @backstage/plugin-app-backend@0.5.12-next.0
- @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.0
- @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.0
- @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.0
- @backstage/plugin-auth-node@0.6.14-next.0
- @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.0
- @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.0
- @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.0
- @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.0
- @backstage/plugin-devtools-backend@0.5.15-next.0
- @backstage/plugin-events-backend@0.5.12-next.0
- @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.0
- @backstage/plugin-kubernetes-backend@0.21.2-next.0
- @backstage/plugin-notifications-backend@0.6.3-next.0
- @backstage/plugin-permission-backend@0.7.10-next.0
- @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.0
- @backstage/plugin-permission-common@0.9.6
- @backstage/plugin-permission-node@0.10.11-next.0
- @backstage/plugin-proxy-backend@0.6.11-next.0
- @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.0
- @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.0
- @backstage/plugin-search-backend@2.0.13-next.0
- @backstage/plugin-search-backend-module-catalog@0.3.13-next.0
- @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.0
- @backstage/plugin-search-backend-module-explore@0.3.12-next.0
- @backstage/plugin-search-backend-module-techdocs@0.4.12-next.0
- @backstage/plugin-search-backend-node@1.4.2-next.0
- @backstage/plugin-signals-backend@0.3.13-next.0
- @backstage/plugin-techdocs-backend@2.1.6-next.0
## 0.0.47
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.0.47",
"version": "0.0.48-next.0",
"backstage": {
"role": "backend"
},
+10
View File
@@ -1,5 +1,15 @@
# @backstage/catalog-client
## 1.13.1-next.0
### Patch Changes
- d2494d6: Minor update to catalog client docs
- Updated dependencies
- @backstage/catalog-model@1.7.6
- @backstage/errors@1.2.7
- @backstage/filter-predicates@0.1.0
## 1.13.0
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-client",
"version": "1.13.0",
"version": "1.13.1-next.0",
"description": "An isomorphic client for the catalog backend",
"backstage": {
"role": "common-library"
+3 -3
View File
@@ -535,9 +535,7 @@ export class CatalogClient implements CatalogApi {
} while (cursor);
}
//
// Private methods
//
// #region Private methods
private async requestIgnored(response: Response): Promise<void> {
if (!response.ok) {
@@ -588,4 +586,6 @@ export class CatalogClient implements CatalogApi {
}
return filters;
}
// #endregion
}
+21 -21
View File
@@ -24,7 +24,7 @@ import { FilterPredicate } from '@backstage/filter-predicates';
/**
* This symbol can be used in place of a value when passed to filters in e.g.
* {@link CatalogClient.getEntities}, to signify that you want to filter on the
* {@link CatalogApi.getEntities}, to signify that you want to filter on the
* presence of that key no matter what its value is.
*
* @public
@@ -146,7 +146,7 @@ export type EntityOrderQuery =
}>;
/**
* The request type for {@link CatalogClient.getEntities}.
* The request type for {@link CatalogApi.getEntities}.
*
* @public
*/
@@ -180,7 +180,7 @@ export interface GetEntitiesRequest {
}
/**
* The response type for {@link CatalogClient.getEntities}.
* The response type for {@link CatalogApi.getEntities}.
*
* @public
*/
@@ -189,7 +189,7 @@ export interface GetEntitiesResponse {
}
/**
* The request type for {@link CatalogClient.getEntitiesByRefs}.
* The request type for {@link CatalogApi.getEntitiesByRefs}.
*
* @public
*/
@@ -200,7 +200,7 @@ export interface GetEntitiesByRefsRequest {
* @remarks
*
* The returned list of entities will be in the same order as the refs, and
* null will be returned in those positions that were not found.
* undefined will be returned in those positions that were not found.
*/
entityRefs: string[];
/**
@@ -215,7 +215,7 @@ export interface GetEntitiesByRefsRequest {
}
/**
* The response type for {@link CatalogClient.getEntitiesByRefs}.
* The response type for {@link CatalogApi.getEntitiesByRefs}.
*
* @public
*/
@@ -226,13 +226,13 @@ export interface GetEntitiesByRefsResponse {
* @remarks
*
* The list will be in the same order as the refs given in the request, and
* null will be returned in those positions that were not found.
* undefined will be returned in those positions that were not found.
*/
items: Array<Entity | undefined>;
}
/**
* The request type for {@link CatalogClient.getEntityAncestors}.
* The request type for {@link CatalogApi.getEntityAncestors}.
*
* @public
*/
@@ -241,7 +241,7 @@ export interface GetEntityAncestorsRequest {
}
/**
* The response type for {@link CatalogClient.getEntityAncestors}.
* The response type for {@link CatalogApi.getEntityAncestors}.
*
* @public
*/
@@ -254,7 +254,7 @@ export interface GetEntityAncestorsResponse {
}
/**
* The request type for {@link CatalogClient.getEntityFacets}.
* The request type for {@link CatalogApi.getEntityFacets}.
*
* @public
*/
@@ -323,7 +323,7 @@ export interface GetEntityFacetsRequest {
}
/**
* The response type for {@link CatalogClient.getEntityFacets}.
* The response type for {@link CatalogApi.getEntityFacets}.
*
* @public
*/
@@ -355,7 +355,7 @@ export type Location = {
};
/**
* The response type for {@link CatalogClient.getLocations}
* The response type for {@link CatalogApi.getLocations}
*
* @public
*/
@@ -364,7 +364,7 @@ export interface GetLocationsResponse {
}
/**
* The request type for {@link CatalogClient.addLocation}.
* The request type for {@link CatalogApi.addLocation}.
*
* @public
*/
@@ -379,7 +379,7 @@ export type AddLocationRequest = {
};
/**
* The response type for {@link CatalogClient.addLocation}.
* The response type for {@link CatalogApi.addLocation}.
*
* @public
*/
@@ -396,7 +396,7 @@ export type AddLocationResponse = {
};
/**
* The response type for {@link CatalogClient.validateEntity}
* The response type for {@link CatalogApi.validateEntity}
*
* @public
*/
@@ -405,7 +405,7 @@ export type ValidateEntityResponse =
| { valid: false; errors: SerializedError[] };
/**
* The request type for {@link CatalogClient.queryEntities}.
* The request type for {@link CatalogApi.queryEntities}.
*
* @public
*/
@@ -414,7 +414,7 @@ export type QueryEntitiesRequest =
| QueryEntitiesCursorRequest;
/**
* A request type for {@link CatalogClient.queryEntities}.
* A request type for {@link CatalogApi.queryEntities}.
* The method takes this type in an initial pagination request,
* when requesting the first batch of entities.
*
@@ -436,7 +436,7 @@ export type QueryEntitiesInitialRequest = {
};
/**
* A request type for {@link CatalogClient.queryEntities}.
* A request type for {@link CatalogApi.queryEntities}.
* The method takes this type in a pagination request, following
* the initial request.
*
@@ -449,7 +449,7 @@ export type QueryEntitiesCursorRequest = {
};
/**
* The response type for {@link CatalogClient.queryEntities}.
* The response type for {@link CatalogApi.queryEntities}.
*
* @public
*/
@@ -467,7 +467,7 @@ export type QueryEntitiesResponse = {
};
/**
* Stream entities request for {@link CatalogClient.streamEntities}.
* Stream entities request for {@link CatalogApi.streamEntities}.
*
* @public
*/
@@ -546,7 +546,7 @@ export interface CatalogApi {
*
* The output list of entities is of the same size and in the same order as
* the requested list of entity refs. Entries that are not found are returned
* as null.
* as undefined.
*
* @param request - Request parameters
* @param options - Additional options
@@ -15,7 +15,9 @@
*/
/**
* This is a copy of the DiscoveryApi, to avoid importing core-plugin-api.
* This is a structurally similar version of `DiscoveryApi` /
* `DiscoveryService`, used here to avoid dependencies on the frontend or
* backend plugin API packages and allowing both of those forms to be passed in.
*/
export type DiscoveryApi = {
getBaseUrl(pluginId: string): Promise<string>;
+3 -1
View File
@@ -15,7 +15,9 @@
*/
/**
* This is a copy of FetchApi, to avoid importing core-plugin-api.
* This is a structurally similar version of `FetchApi`, used here to avoid
* dependencies on the frontend or backend plugin API packages and allowing both
* of those forms to be passed in.
*/
export type FetchApi = {
fetch: typeof fetch;
+34
View File
@@ -1,5 +1,39 @@
# @backstage/cli-common
## 0.2.0-next.0
### Minor Changes
- 56bd494: Added `targetPaths` and `findOwnPaths` as replacements for `findPaths`, with a cleaner separation between target project paths and package-relative paths.
To migrate existing `findPaths` usage:
```ts
// Before
import { findPaths } from '@backstage/cli-common';
const paths = findPaths(__dirname);
// After — for target project paths (cwd-based):
import { targetPaths } from '@backstage/cli-common';
// paths.targetDir → targetPaths.dir
// paths.targetRoot → targetPaths.rootDir
// paths.resolveTarget('src') → targetPaths.resolve('src')
// paths.resolveTargetRoot('yarn.lock') → targetPaths.resolveRoot('yarn.lock')
// After — for package-relative paths:
import { findOwnPaths } from '@backstage/cli-common';
const own = findOwnPaths(__dirname);
// paths.ownDir → own.dir
// paths.ownRoot → own.rootDir
// paths.resolveOwn('config/jest.js') → own.resolve('config/jest.js')
// paths.resolveOwnRoot('tsconfig.json') → own.resolveRoot('tsconfig.json')
```
### Patch Changes
- Updated dependencies
- @backstage/errors@1.2.7
## 0.1.18
### Patch Changes
+17 -4
View File
@@ -1,14 +1,12 @@
{
"name": "@backstage/cli-common",
"version": "0.1.18",
"version": "0.2.0-next.0",
"description": "Common functionality used by cli, backend, and create-app",
"backstage": {
"role": "node-library"
},
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
"access": "public"
},
"keywords": [
"backstage"
@@ -20,8 +18,23 @@
"directory": "packages/cli-common"
},
"license": "Apache-2.0",
"exports": {
".": "./src/index.ts",
"./testUtils": "./src/testUtils.ts",
"./package.json": "./package.json"
},
"main": "src/index.ts",
"types": "src/index.ts",
"typesVersions": {
"*": {
"testUtils": [
"src/testUtils.ts"
],
"package.json": [
"package.json"
]
}
},
"files": [
"dist"
],
@@ -0,0 +1,23 @@
## API Report File for "@backstage/cli-common"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
// @public
export function overrideTargetPaths(
dirOrOptions: string | OverrideTargetPathsOptions,
): TargetPathsOverride;
// @public
export interface OverrideTargetPathsOptions {
dir: string;
rootDir?: string;
}
// @public
export interface TargetPathsOverride {
restore(): void;
}
// (No @packageDocumentation comment for this package)
```
+22 -1
View File
@@ -16,17 +16,27 @@ export function bootstrapEnvProxyAgents(): void;
// @public
export class ExitCodeError extends CustomErrorBase {
constructor(code: number, command?: string);
// (undocumented)
readonly code: number;
}
// @public
export function findOwnPaths(searchDir: string): OwnPaths;
// @public @deprecated
export function findPaths(searchDir: string): Paths;
// @public
export function isChildPath(base: string, path: string): boolean;
// @public
export type OwnPaths = {
dir: string;
rootDir: string;
resolve: ResolveFunc;
resolveRoot: ResolveFunc;
};
// @public @deprecated
export type Paths = {
ownDir: string;
ownRoot: string;
@@ -68,4 +78,15 @@ export function runOutput(
args: string[],
options?: RunOptions,
): Promise<string>;
// @public
export type TargetPaths = {
dir: string;
rootDir: string;
resolve: ResolveFunc;
resolveRoot: ResolveFunc;
};
// @public
export const targetPaths: TargetPaths;
```
+1
View File
@@ -21,6 +21,7 @@ import { CustomErrorBase } from '@backstage/errors';
* @public
*/
export class ExitCodeError extends CustomErrorBase {
/** The exit code of the child process. */
readonly code: number;
constructor(code: number, command?: string) {
+2 -2
View File
@@ -20,9 +20,9 @@
* @packageDocumentation
*/
export { findPaths, BACKSTAGE_JSON } from './paths';
export { findPaths, findOwnPaths, targetPaths, BACKSTAGE_JSON } from './paths';
export { isChildPath } from './isChildPath';
export type { Paths, ResolveFunc } from './paths';
export type { Paths, TargetPaths, OwnPaths, ResolveFunc } from './paths';
export { bootstrapEnvProxyAgents } from './proxyBootstrap';
export {
run,
+231 -71
View File
@@ -25,35 +25,61 @@ import { dirname, resolve as resolvePath } from 'node:path';
*/
export type ResolveFunc = (...paths: string[]) => string;
/**
* Resolved paths relative to the target project, based on `process.cwd()`.
* Lazily initialized on first property access. Re-resolves automatically
* when `process.cwd()` changes.
*
* @public
*/
export type TargetPaths = {
/** The target package directory. */
dir: string;
/** The target monorepo root directory. */
rootDir: string;
/** Resolve a path relative to the target package directory. */
resolve: ResolveFunc;
/** Resolve a path relative to the target repo root. */
resolveRoot: ResolveFunc;
};
/**
* Resolved paths relative to a specific package in the repository.
*
* @public
*/
export type OwnPaths = {
/** The package root directory. */
dir: string;
/** The monorepo root directory containing the package. */
rootDir: string;
/** Resolve a path relative to the package root. */
resolve: ResolveFunc;
/** Resolve a path relative to the monorepo root containing the package. */
resolveRoot: ResolveFunc;
};
/**
* Common paths and resolve functions used by the cli.
* Currently assumes it is being executed within a monorepo.
*
* @public
* @deprecated Use {@link targetPaths} and {@link findOwnPaths} instead.
*/
export type Paths = {
// Root dir of the cli itself, containing package.json
ownDir: string;
// Monorepo root dir of the cli itself. Only accessible when running inside Backstage repo.
ownRoot: string;
// The location of the app that the cli is being executed in
targetDir: string;
// The monorepo root package of the app that the cli is being executed in.
targetRoot: string;
// Resolve a path relative to own repo
resolveOwn: ResolveFunc;
// Resolve a path relative to own monorepo root. Only accessible when running inside Backstage repo.
resolveOwnRoot: ResolveFunc;
// Resolve a path relative to the app
resolveTarget: ResolveFunc;
// Resolve a path relative to the app repo root
resolveTargetRoot: ResolveFunc;
};
@@ -84,18 +110,7 @@ export function findRootPath(
);
}
// Finds the root of a given package
export function findOwnDir(searchDir: string) {
const path = findRootPath(searchDir, () => true);
if (!path) {
throw new Error(
`No package.json found while searching for package root of ${searchDir}`,
);
}
return path;
}
// Finds the root of the monorepo that the package exists in. Only accessible when running inside Backstage repo.
// Finds the root of the monorepo that the package exists in.
export function findOwnRootDir(ownDir: string) {
const isLocal = fs.existsSync(resolvePath(ownDir, 'src'));
if (!isLocal) {
@@ -107,64 +122,209 @@ export function findOwnRootDir(ownDir: string) {
return resolvePath(ownDir, '../..');
}
// Hierarchical directory cache shared across all OwnPathsImpl instances.
// When we resolve a searchDir to its package root, we also cache every
// intermediate directory, so sibling directories share work.
const dirCache = new Map<string, string>();
class OwnPathsImpl implements OwnPaths {
static #instanceCache = new Map<string, OwnPathsImpl>();
static find(searchDir: string): OwnPathsImpl {
const dir = OwnPathsImpl.findDir(searchDir);
let instance = OwnPathsImpl.#instanceCache.get(dir);
if (!instance) {
instance = new OwnPathsImpl(dir);
OwnPathsImpl.#instanceCache.set(dir, instance);
}
return instance;
}
static findDir(searchDir: string): string {
const visited: string[] = [];
let dir = searchDir;
for (let i = 0; i < 1000; i++) {
const cached = dirCache.get(dir);
if (cached !== undefined) {
for (const d of visited) {
dirCache.set(d, cached);
}
return cached;
}
visited.push(dir);
if (fs.existsSync(resolvePath(dir, 'package.json'))) {
for (const d of visited) {
dirCache.set(d, dir);
}
return dir;
}
const newDir = dirname(dir);
if (newDir === dir) {
break;
}
dir = newDir;
}
throw new Error(
`No package.json found while searching for package root of ${searchDir}`,
);
}
#dir: string;
#rootDir: string | undefined;
private constructor(dir: string) {
this.#dir = dir;
}
get dir(): string {
return this.#dir;
}
get rootDir(): string {
this.#rootDir ??= findOwnRootDir(this.#dir);
return this.#rootDir;
}
resolve = (...paths: string[]): string => {
return resolvePath(this.#dir, ...paths);
};
resolveRoot = (...paths: string[]): string => {
return resolvePath(this.rootDir, ...paths);
};
}
// Finds the root of a given package
export function findOwnDir(searchDir: string) {
return OwnPathsImpl.findDir(searchDir);
}
// Used by the test utility in testUtils.ts to override targetPaths
export let targetPathsOverride: TargetPaths | undefined;
/** @internal */
export function setTargetPathsOverride(override: TargetPaths | undefined) {
targetPathsOverride = override;
}
class TargetPathsImpl implements TargetPaths {
#cwd: string | undefined;
#dir: string | undefined;
#rootDir: string | undefined;
get dir(): string {
if (targetPathsOverride) {
return targetPathsOverride.dir;
}
const cwd = process.cwd();
if (this.#dir !== undefined && this.#cwd === cwd) {
return this.#dir;
}
this.#cwd = cwd;
this.#rootDir = undefined;
// Drive letter can end up being lowercased here on Windows, bring back to uppercase for consistency
this.#dir = fs
.realpathSync(cwd)
.replace(/^[a-z]:/, str => str.toLocaleUpperCase('en-US'));
return this.#dir;
}
get rootDir(): string {
if (targetPathsOverride) {
return targetPathsOverride.rootDir;
}
// Access dir first to ensure cwd is fresh, which also invalidates rootDir on cwd change
const dir = this.dir;
if (this.#rootDir !== undefined) {
return this.#rootDir;
}
// Lazy init to only crash commands that require a monorepo when we're not in one
this.#rootDir =
findRootPath(dir, path => {
try {
const content = fs.readFileSync(path, 'utf8');
const data = JSON.parse(content);
return Boolean(data.workspaces);
} catch (error) {
throw new Error(
`Failed to parse package.json file while searching for root, ${error}`,
);
}
}) ?? dir;
return this.#rootDir;
}
resolve = (...paths: string[]): string => {
if (targetPathsOverride) {
return targetPathsOverride.resolve(...paths);
}
return resolvePath(this.dir, ...paths);
};
resolveRoot = (...paths: string[]): string => {
if (targetPathsOverride) {
return targetPathsOverride.resolveRoot(...paths);
}
return resolvePath(this.rootDir, ...paths);
};
}
/**
* Lazily resolved paths relative to the target project. Import this directly
* for cwd-based path resolution without needing `__dirname`.
*
* @public
*/
export const targetPaths: TargetPaths = new TargetPathsImpl();
/**
* Find paths relative to the package that the calling code lives in.
*
* Results are cached per package root, and the package root lookup uses a
* hierarchical directory cache so that multiple calls from different
* subdirectories within the same package share work.
*
* @public
*/
export function findOwnPaths(searchDir: string): OwnPaths {
return OwnPathsImpl.find(searchDir);
}
/**
* Find paths related to a package and its execution context.
*
* @public
* @deprecated Use {@link targetPaths} for cwd-based paths and
* {@link findOwnPaths} for package-relative paths instead.
*
* @example
*
* const paths = findPaths(__dirname)
*/
export function findPaths(searchDir: string): Paths {
const ownDir = findOwnDir(searchDir);
// Drive letter can end up being lowercased here on Windows, bring back to uppercase for consistency
const targetDir = fs
.realpathSync(process.cwd())
.replace(/^[a-z]:/, str => str.toLocaleUpperCase('en-US'));
// Lazy load this as it will throw an error if we're not inside the Backstage repo.
let ownRoot = '';
const getOwnRoot = () => {
if (!ownRoot) {
ownRoot = findOwnRootDir(ownDir);
}
return ownRoot;
};
// We're not always running in a monorepo, so we lazy init this to only crash commands
// that require a monorepo when we're not in one.
let targetRoot = '';
const getTargetRoot = () => {
if (!targetRoot) {
targetRoot =
findRootPath(targetDir, path => {
try {
const content = fs.readFileSync(path, 'utf8');
const data = JSON.parse(content);
return Boolean(data.workspaces);
} catch (error) {
throw new Error(
`Failed to parse package.json file while searching for root, ${error}`,
);
}
}) ?? targetDir; // We didn't find any root package.json, assume we're not in a monorepo
}
return targetRoot;
};
const own = findOwnPaths(searchDir);
return {
ownDir,
get ownDir() {
return own.dir;
},
get ownRoot() {
return getOwnRoot();
return own.rootDir;
},
get targetDir() {
return targetPaths.dir;
},
targetDir,
get targetRoot() {
return getTargetRoot();
return targetPaths.rootDir;
},
resolveOwn: (...paths) => resolvePath(ownDir, ...paths),
resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths),
resolveTarget: (...paths) => resolvePath(targetDir, ...paths),
resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths),
resolveOwn: own.resolve,
resolveOwnRoot: own.resolveRoot,
resolveTarget: targetPaths.resolve,
resolveTargetRoot: targetPaths.resolveRoot,
};
}
+78
View File
@@ -0,0 +1,78 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { resolve as resolvePath } from 'node:path';
import { setTargetPathsOverride } from './paths';
/**
* Options for {@link overrideTargetPaths}.
*
* @public
*/
export interface OverrideTargetPathsOptions {
/** The target package directory. */
dir: string;
/** The target monorepo root directory. Defaults to `dir` if not provided. */
rootDir?: string;
}
/**
* Return value of {@link overrideTargetPaths}.
*
* @public
*/
export interface TargetPathsOverride {
/** Restores `targetPaths` to its normal behavior. */
restore(): void;
}
/**
* Overrides the `targetPaths` singleton to resolve from the given directory
* instead of `process.cwd()`.
*
* When called with a string, that value is used as both `dir` and `rootDir`.
* Pass an options object to set them independently.
*
* Calling `restore()` on the return value reverts to normal behavior.
* Restoration is only needed if you want to change the override within a
* test file; each Jest worker starts with a clean module state.
*
* @public
*/
export function overrideTargetPaths(
dirOrOptions: string | OverrideTargetPathsOptions,
): TargetPathsOverride {
const { dir, rootDir } =
typeof dirOrOptions === 'string'
? { dir: dirOrOptions, rootDir: dirOrOptions }
: {
dir: dirOrOptions.dir,
rootDir: dirOrOptions.rootDir ?? dirOrOptions.dir,
};
setTargetPathsOverride({
dir,
rootDir,
resolve: (...paths: string[]) => resolvePath(dir, ...paths),
resolveRoot: (...paths: string[]) => resolvePath(rootDir, ...paths),
});
return {
restore() {
setTargetPathsOverride(undefined);
},
};
}
+11
View File
@@ -1,5 +1,16 @@
# @backstage/cli-node
## 0.2.19-next.0
### Patch Changes
- 06c2015: Added `runConcurrentTasks` and `runWorkerQueueThreads` utilities, moved from the `@backstage/cli` internal code.
- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`.
- Updated dependencies
- @backstage/cli-common@0.2.0-next.0
- @backstage/errors@1.2.7
- @backstage/types@1.2.2
## 0.2.18
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/cli-node",
"version": "0.2.18",
"version": "0.2.19-next.0",
"description": "Node.js library for Backstage CLIs",
"backstage": {
"role": "node-library"
+30
View File
@@ -86,6 +86,13 @@ export interface BackstagePackageJson {
version: string;
}
// @public
export type ConcurrentTasksOptions<TItem> = {
concurrencyFactor?: number;
items: Iterable<TItem>;
worker: (item: TItem) => Promise<void>;
};
// @public
export class GitUtils {
static listChangedFiles(ref: string): Promise<string[]>;
@@ -200,4 +207,27 @@ export class PackageRoles {
static getRoleFromPackage(pkgJson: unknown): PackageRole | undefined;
static getRoleInfo(role: string): PackageRoleInfo;
}
// @public
export function runConcurrentTasks<TItem>(
options: ConcurrentTasksOptions<TItem>,
): Promise<void>;
// @public
export function runWorkerQueueThreads<TItem, TResult, TContext>(
options: WorkerQueueThreadsOptions<TItem, TResult, TContext>,
): Promise<{
results: TResult[];
}>;
// @public
export type WorkerQueueThreadsOptions<TItem, TResult, TContext> = {
items: Iterable<TItem>;
workerFactory: (
context: TContext,
) =>
| ((item: TItem) => Promise<TResult>)
| Promise<(item: TItem) => Promise<TResult>>;
context?: TContext;
};
```
@@ -0,0 +1,70 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import os from 'node:os';
const defaultConcurrency = Math.max(Math.ceil(os.cpus().length / 2), 1);
const CONCURRENCY_ENV_VAR = 'BACKSTAGE_CLI_CONCURRENCY';
const DEPRECATED_CONCURRENCY_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL';
type ConcurrencyOption = boolean | string | number | null | undefined;
function parseConcurrencyOption(value: ConcurrencyOption): number {
if (value === undefined || value === null) {
return defaultConcurrency;
} else if (typeof value === 'boolean') {
return value ? defaultConcurrency : 1;
} else if (typeof value === 'number' && Number.isInteger(value)) {
if (value < 1) {
return 1;
}
return value;
} else if (typeof value === 'string') {
if (value === 'true') {
return parseConcurrencyOption(true);
} else if (value === 'false') {
return parseConcurrencyOption(false);
}
const parsed = Number(value);
if (Number.isInteger(parsed)) {
return parseConcurrencyOption(parsed);
}
}
throw Error(
`Concurrency option value '${value}' is not a boolean or integer`,
);
}
let hasWarnedDeprecation = false;
/** @internal */
export function getEnvironmentConcurrency() {
if (process.env[CONCURRENCY_ENV_VAR] !== undefined) {
return parseConcurrencyOption(process.env[CONCURRENCY_ENV_VAR]);
}
if (process.env[DEPRECATED_CONCURRENCY_ENV_VAR] !== undefined) {
if (!hasWarnedDeprecation) {
hasWarnedDeprecation = true;
console.warn(
`The ${DEPRECATED_CONCURRENCY_ENV_VAR} environment variable is deprecated, use ${CONCURRENCY_ENV_VAR} instead`,
);
}
return parseConcurrencyOption(process.env[DEPRECATED_CONCURRENCY_ENV_VAR]);
}
return defaultConcurrency;
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { findPaths } from '@backstage/cli-common';
/* eslint-disable-next-line no-restricted-syntax */
export const paths = findPaths(__dirname);
export type { ConcurrentTasksOptions } from './runConcurrentTasks';
export { runConcurrentTasks } from './runConcurrentTasks';
export type { WorkerQueueThreadsOptions } from './runWorkerQueueThreads';
export { runWorkerQueueThreads } from './runWorkerQueueThreads';

Some files were not shown because too many files have changed in this diff Show More