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
+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';
@@ -0,0 +1,144 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { runConcurrentTasks } from './runConcurrentTasks';
describe('runConcurrentTasks', () => {
afterEach(() => {
delete process.env.BACKSTAGE_CLI_CONCURRENCY;
});
it('executes work in parallel', async () => {
const started = new Array<number>();
const done = new Array<number>();
const waiting = new Array<() => void>();
process.env.BACKSTAGE_CLI_CONCURRENCY = '4';
const work = runConcurrentTasks({
items: [0, 1, 2, 3, 4],
concurrencyFactor: 0.5, // 2 at a time
worker: async item => {
started.push(item);
await new Promise<void>(resolve => {
waiting[item] = resolve;
});
done.push(item);
},
});
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1]);
expect(done).toEqual([]);
waiting[0]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2]);
expect(done).toEqual([0]);
waiting[1]();
waiting[2]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2, 3, 4]);
expect(done).toEqual([0, 1, 2]);
waiting[3]();
waiting[4]();
await work;
expect(done).toEqual([0, 1, 2, 3, 4]);
});
it('executes work sequentially', async () => {
const started = new Array<number>();
const done = new Array<number>();
const waiting = new Array<() => void>();
const work = runConcurrentTasks({
items: [0, 1, 2, 3, 4],
concurrencyFactor: 0, // 1 at a time
worker: async item => {
started.push(item);
await new Promise<void>(resolve => {
waiting[item] = resolve;
});
done.push(item);
},
});
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0]);
expect(done).toEqual([]);
waiting[0]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1]);
expect(done).toEqual([0]);
waiting[1]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2]);
waiting[2]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2, 3]);
waiting[3]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2, 3, 4]);
waiting[4]();
await work;
expect(done).toEqual([0, 1, 2, 3, 4]);
});
it('returns void', async () => {
const result = await runConcurrentTasks({
items: [1, 2, 3],
worker: async () => {},
});
expect(result).toBeUndefined();
});
it('defaults to environment concurrency', async () => {
const started = new Array<number>();
process.env.BACKSTAGE_CLI_CONCURRENCY = '2';
await runConcurrentTasks({
items: [0, 1],
worker: async item => {
started.push(item);
},
});
expect(started).toEqual([0, 1]);
});
it('supports the deprecated BACKSTAGE_CLI_BUILD_PARALLEL with a warning', async () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '2';
await runConcurrentTasks({
items: [0, 1],
worker: async () => {},
});
expect(warnSpy).toHaveBeenCalledWith(
'The BACKSTAGE_CLI_BUILD_PARALLEL environment variable is deprecated, use BACKSTAGE_CLI_CONCURRENCY instead',
);
delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL;
warnSpy.mockRestore();
});
});
@@ -0,0 +1,62 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getEnvironmentConcurrency } from './concurrency';
/**
* Options for {@link runConcurrentTasks}.
*
* @public
*/
export type ConcurrentTasksOptions<TItem> = {
/**
* Decides the number of concurrent workers by multiplying
* this with the configured concurrency.
*
* Defaults to 1.
*/
concurrencyFactor?: number;
items: Iterable<TItem>;
worker: (item: TItem) => Promise<void>;
};
/**
* Runs items through a worker function concurrently across multiple async workers.
*
* @public
*/
export async function runConcurrentTasks<TItem>(
options: ConcurrentTasksOptions<TItem>,
): Promise<void> {
const { concurrencyFactor = 1, items, worker } = options;
const concurrency = getEnvironmentConcurrency();
const sharedIterator = items[Symbol.iterator]();
const sharedIterable = {
[Symbol.iterator]: () => sharedIterator,
};
const workerCount = Math.max(Math.floor(concurrencyFactor * concurrency), 1);
await Promise.all(
Array(workerCount)
.fill(0)
.map(async () => {
for (const value of sharedIterable) {
await worker(value);
}
}),
);
}
@@ -0,0 +1,42 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { runWorkerQueueThreads } from './runWorkerQueueThreads';
describe('runWorkerQueueThreads', () => {
it('should execute work in parallel', async () => {
const sharedData = new SharedArrayBuffer(10);
const sharedView = new Uint8Array(sharedData);
const { results } = await runWorkerQueueThreads({
context: sharedData,
items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
workerFactory: (data: SharedArrayBuffer) => {
const view = new Uint8Array(data);
return async (i: number) => {
view[i] = 10 + i;
return 20 + i;
};
},
});
expect(Array.from(sharedView)).toEqual([
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
]);
expect(results).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29]);
});
});
@@ -0,0 +1,175 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ErrorLike } from '@backstage/errors';
import { Worker } from 'node:worker_threads';
import { getEnvironmentConcurrency } from './concurrency';
type WorkerThreadMessage =
| {
type: 'done';
}
| {
type: 'item';
index: number;
item: unknown;
}
| {
type: 'start';
}
| {
type: 'result';
index: number;
result: unknown;
}
| {
type: 'error';
error: ErrorLike;
};
/**
* Options for {@link runWorkerQueueThreads}.
*
* @public
*/
export type WorkerQueueThreadsOptions<TItem, TResult, TContext> = {
/** The items to process */
items: Iterable<TItem>;
/**
* A function that will be called within each worker thread at startup,
* which should return the worker function that will be called for each item.
*
* This function must be defined as an arrow function or using the
* function keyword, and must be entirely self contained, not referencing
* any variables outside of its scope. This is because the function source
* is stringified and evaluated in the worker thread.
*
* To pass data to the worker, use the `context` option and `items`, but
* note that they are both copied by value into the worker thread, except for
* types that are explicitly shareable across threads, such as `SharedArrayBuffer`.
*/
workerFactory: (
context: TContext,
) =>
| ((item: TItem) => Promise<TResult>)
| Promise<(item: TItem) => Promise<TResult>>;
/** Context data supplied to each worker factory */
context?: TContext;
};
/**
* Spawns one or more worker threads using the `worker_threads` module.
* Each thread processes one item at a time from the provided `options.items`.
*
* @public
*/
export async function runWorkerQueueThreads<TItem, TResult, TContext>(
options: WorkerQueueThreadsOptions<TItem, TResult, TContext>,
): Promise<{ results: TResult[] }> {
const items = Array.from(options.items);
const workerFactory = options.workerFactory;
const workerData = options.context;
const threadCount = Math.min(getEnvironmentConcurrency(), items.length);
const iterator = items[Symbol.iterator]();
const results = new Array<TResult>();
let itemIndex = 0;
await Promise.all(
Array(threadCount)
.fill(0)
.map(async () => {
const thread = new Worker(`(${workerQueueThread})(${workerFactory})`, {
eval: true,
workerData,
});
return new Promise<void>((resolve, reject) => {
thread.on('message', (message: WorkerThreadMessage) => {
if (message.type === 'start' || message.type === 'result') {
if (message.type === 'result') {
results[message.index] = message.result as TResult;
}
const { value, done } = iterator.next();
if (done) {
thread.postMessage({ type: 'done' });
} else {
thread.postMessage({
type: 'item',
index: itemIndex,
item: value,
});
itemIndex += 1;
}
} else if (message.type === 'error') {
const error = new Error(message.error.message);
error.name = message.error.name;
error.stack = message.error.stack;
reject(error);
}
});
thread.on('error', reject);
thread.on('exit', (code: number) => {
if (code !== 0) {
reject(new Error(`Worker thread exited with code ${code}`));
} else {
resolve();
}
});
});
}),
);
return { results };
}
/* istanbul ignore next */
function workerQueueThread(
workerFuncFactory: (
data: unknown,
) => Promise<(item: unknown) => Promise<unknown>>,
) {
const { parentPort, workerData } = require('node:worker_threads');
Promise.resolve()
.then(() => workerFuncFactory(workerData))
.then(
workerFunc => {
parentPort.on('message', async (message: WorkerThreadMessage) => {
if (message.type === 'done') {
parentPort.close();
return;
}
if (message.type === 'item') {
try {
const result = await workerFunc(message.item);
parentPort.postMessage({
type: 'result',
index: message.index,
result,
});
} catch (error) {
parentPort.postMessage({ type: 'error', error });
}
}
});
parentPort.postMessage({ type: 'start' });
},
error => parentPort.postMessage({ type: 'error', error }),
);
}
+3 -3
View File
@@ -15,7 +15,7 @@
*/
import { assertError, ForwardedError } from '@backstage/errors';
import { paths } from '../paths';
import { targetPaths } from '@backstage/cli-common';
import { runOutput } from '@backstage/cli-common';
/**
@@ -24,7 +24,7 @@ import { runOutput } from '@backstage/cli-common';
export async function runGit(...args: string[]) {
try {
const stdout = await runOutput(['git', ...args], {
cwd: paths.targetRoot,
cwd: targetPaths.rootDir,
});
return stdout.trim().split(/\r\n|\r|\n/);
} catch (error) {
@@ -88,7 +88,7 @@ export class GitUtils {
}
const stdout = await runOutput(['git', 'show', `${showRef}:${path}`], {
cwd: paths.targetRoot,
cwd: targetPaths.rootDir,
});
return stdout;
}
+1
View File
@@ -22,4 +22,5 @@
export * from './git';
export * from './monorepo';
export * from './concurrency';
export * from './roles';
@@ -14,21 +14,16 @@
* limitations under the License.
*/
import { resolve as resolvePath } from 'node:path';
import { getPackages } from '@manypkg/get-packages';
import { PackageGraph } from './PackageGraph';
import { Lockfile } from './Lockfile';
import { GitUtils } from '../git';
import { overrideTargetPaths } from '@backstage/cli-common/testUtils';
const mockListChangedFiles = jest.spyOn(GitUtils, 'listChangedFiles');
const mockReadFileAtRef = jest.spyOn(GitUtils, 'readFileAtRef');
jest.mock('../paths', () => ({
paths: {
targetRoot: '/',
resolveTargetRoot: (...paths: string[]) => resolvePath('/', ...paths),
},
}));
overrideTargetPaths('/');
const testPackages = [
{
@@ -16,7 +16,7 @@
import path from 'node:path';
import { getPackages, Package } from '@manypkg/get-packages';
import { paths } from '../paths';
import { targetPaths } from '@backstage/cli-common';
import { PackageRole } from '../roles';
import { GitUtils } from '../git';
import { Lockfile } from './Lockfile';
@@ -192,7 +192,7 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
* Lists all local packages in a monorepo.
*/
static async listTargetPackages(): Promise<BackstagePackage[]> {
const { packages } = await getPackages(paths.targetDir);
const { packages } = await getPackages(targetPaths.dir);
return packages as BackstagePackage[];
}
@@ -332,7 +332,7 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
Array.from(this.values()).map(pkg => [
// relative from root, convert to posix, and add a / at the end
path
.relative(paths.targetRoot, pkg.dir)
.relative(targetPaths.rootDir, pkg.dir)
.split(path.sep)
.join(path.posix.sep) + path.posix.sep,
pkg,
@@ -374,7 +374,7 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
let otherLockfile: Lockfile;
try {
thisLockfile = await Lockfile.load(
paths.resolveTargetRoot('yarn.lock'),
targetPaths.resolveRoot('yarn.lock'),
);
otherLockfile = Lockfile.parse(
await GitUtils.readFileAtRef('yarn.lock', options.ref),
+2 -2
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { paths } from '../paths';
import { targetPaths } from '@backstage/cli-common';
import fs from 'fs-extra';
/**
@@ -26,7 +26,7 @@ import fs from 'fs-extra';
* @public
*/
export async function isMonoRepo(): Promise<boolean> {
const rootPackageJsonPath = paths.resolveTargetRoot('package.json');
const rootPackageJsonPath = targetPaths.resolveRoot('package.json');
try {
const pkg = await fs.readJson(rootPackageJsonPath);
return Boolean(pkg?.workspaces);
@@ -16,12 +16,10 @@
import { isMonoRepo } from './isMonoRepo';
import { createMockDirectory } from '@backstage/backend-test-utils';
import { overrideTargetPaths } from '@backstage/cli-common/testUtils';
const mockDir = createMockDirectory();
jest.mock('../paths', () => ({
paths: { resolveTargetRoot: (...args: string[]) => mockDir.resolve(...args) },
}));
overrideTargetPaths(mockDir.path);
describe('isMonoRepo', () => {
it('should detect a monorepo', async () => {
@@ -15,16 +15,13 @@
*/
import { createMockDirectory } from '@backstage/backend-test-utils';
import { overrideTargetPaths } from '@backstage/cli-common/testUtils';
import { detectPackageManager } from './PackageManager';
import { Yarn } from './yarn';
import { withLogCollector } from '@backstage/test-utils';
const mockDir = createMockDirectory();
jest.mock('../paths', () => ({
...jest.requireActual('../paths'),
paths: { resolveTargetRoot: (...args: string[]) => mockDir.resolve(...args) },
}));
overrideTargetPaths(mockDir.path);
const mockYarnCreate = jest.spyOn(Yarn, 'create');
@@ -16,7 +16,7 @@
import { Yarn } from './yarn';
import { Lockfile } from './Lockfile';
import { paths } from '../paths';
import { targetPaths } from '@backstage/cli-common';
import { RunOptions } from '@backstage/cli-common';
import fs from 'fs-extra';
@@ -85,7 +85,7 @@ export interface PackageManager {
*/
export async function detectPackageManager(): Promise<PackageManager> {
const hasYarnLockfile = await fileExists(
paths.resolveTargetRoot('yarn.lock'),
targetPaths.resolveRoot('yarn.lock'),
);
if (hasYarnLockfile) {
return await Yarn.create();
@@ -93,7 +93,7 @@ export async function detectPackageManager(): Promise<PackageManager> {
try {
const packageJson = await fs.readJson(
paths.resolveTargetRoot('package.json'),
targetPaths.resolveRoot('package.json'),
);
if (packageJson.workspaces) {
// technically this could be NPM as well
+29
View File
@@ -1,5 +1,34 @@
# @backstage/cli
## 0.35.5-next.0
### Patch Changes
- 246877a: Updated dependency `bfj` to `^9.0.2`.
- bba2e49: Internal refactor to use new concurrency utilities from `@backstage/cli-node`.
- fd50cb3: Added `translations export` and `translations import` commands for managing translation files.
The `translations export` command discovers all `TranslationRef` definitions across frontend plugin dependencies and exports their default messages as JSON files. The `translations import` command generates `TranslationResource` wiring code from translated JSON files, ready to be plugged into the app.
Both commands support a `--pattern` option for controlling the message file layout, for example `--pattern '{lang}/{id}.json'` for language-based directory grouping.
- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1
- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`.
- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages.
- 092b41f: Updated dependency `webpack` to `~5.105.0`.
- Updated dependencies
- @backstage/cli-common@0.2.0-next.0
- @backstage/cli-node@0.2.19-next.0
- @backstage/eslint-plugin@0.2.2-next.0
- @backstage/integration@1.21.0-next.0
- @backstage/config-loader@1.10.9-next.0
- @backstage/catalog-model@1.7.6
- @backstage/config@1.3.6
- @backstage/errors@1.2.7
- @backstage/module-federation-common@0.1.0
- @backstage/release-manifests@0.0.13
- @backstage/types@1.2.2
## 0.35.4
### Patch Changes
+39
View File
@@ -25,6 +25,7 @@ Commands:
new
package [command]
repo [command]
translations [command]
versions:bump
versions:migrate
```
@@ -542,6 +543,44 @@ Options:
-h, --help
```
### `backstage-cli translations`
```
Usage: backstage-cli translations [options] [command] [command]
Options:
-h, --help
Commands:
export
help [command]
import
```
### `backstage-cli translations export`
```
Usage: <none>
Options:
--help
--output
--pattern
--version
```
### `backstage-cli translations import`
```
Usage: <none>
Options:
--help
--input
--output
--version
```
### `backstage-cli versions:bump`
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/cli",
"version": "0.35.4",
"version": "0.35.5-next.0",
"description": "CLI for developing Backstage plugins and apps",
"backstage": {
"role": "cli"
+1
View File
@@ -27,5 +27,6 @@ import { CliInitializer } from './wiring/CliInitializer';
initializer.add(import('./modules/migrate'));
initializer.add(import('./modules/new'));
initializer.add(import('./modules/test'));
initializer.add(import('./modules/translations'));
await initializer.run();
})();
+2 -2
View File
@@ -16,7 +16,7 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'node:path';
import { paths } from '../paths';
import { targetPaths } from '@backstage/cli-common';
const DEFAULT_CACHE_BASE_PATH = 'node_modules/.cache/backstage-cli';
@@ -31,7 +31,7 @@ export class SuccessCache {
* location.
*/
static trimPaths(input: string) {
return input.replaceAll(paths.targetRoot, '');
return input.replaceAll(targetPaths.rootDir, '');
}
constructor(name: string, basePath?: string) {
-215
View File
@@ -1,215 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import os from 'node:os';
import {
parseParallelismOption,
getEnvironmentParallelism,
runParallelWorkers,
runWorkerQueueThreads,
runWorkerThreads,
} from './parallel';
const defaultParallelism = Math.ceil(os.cpus().length / 2);
describe('parseParallelismOption', () => {
it('coerces false no parallelism', () => {
expect(parseParallelismOption(false)).toBe(1);
expect(parseParallelismOption('false')).toBe(1);
});
it('coerces true or undefined to default parallelism', () => {
expect(parseParallelismOption(true)).toBe(defaultParallelism);
expect(parseParallelismOption('true')).toBe(defaultParallelism);
expect(parseParallelismOption(undefined)).toBe(defaultParallelism);
expect(parseParallelismOption(null)).toBe(defaultParallelism);
});
it('coerces number string to number', () => {
expect(parseParallelismOption('2')).toBe(2);
});
it.each([['on'], [2.5], ['2.5']])('throws error for %p', value => {
expect(() => parseParallelismOption(value as any)).toThrow(
`Parallel option value '${value}' is not a boolean or integer`,
);
});
});
describe('getEnvironmentParallelism', () => {
it('reads the parallelism setting from the environment', () => {
process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '2';
expect(getEnvironmentParallelism()).toBe(2);
process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'true';
expect(getEnvironmentParallelism()).toBe(defaultParallelism);
process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'false';
expect(getEnvironmentParallelism()).toBe(1);
delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL;
expect(getEnvironmentParallelism()).toBe(defaultParallelism);
});
});
describe('runParallelWorkers', () => {
it('executes work in parallel', async () => {
const started = new Array<number>();
const done = new Array<number>();
const waiting = new Array<() => void>();
const work = runParallelWorkers({
items: [0, 1, 2, 3, 4],
parallelismSetting: 4,
parallelismFactor: 0.5, // 2 at a time
worker: async item => {
started.push(item);
await new Promise<void>(resolve => {
waiting[item] = resolve;
});
done.push(item);
},
});
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1]);
expect(done).toEqual([]);
waiting[0]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2]);
expect(done).toEqual([0]);
waiting[1]();
waiting[2]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2, 3, 4]);
expect(done).toEqual([0, 1, 2]);
waiting[3]();
waiting[4]();
await work;
expect(done).toEqual([0, 1, 2, 3, 4]);
});
it('executes work sequentially', async () => {
const started = new Array<number>();
const done = new Array<number>();
const waiting = new Array<() => void>();
const work = runParallelWorkers({
items: [0, 1, 2, 3, 4],
parallelismFactor: 0, // 1 at a time
worker: async item => {
started.push(item);
await new Promise<void>(resolve => {
waiting[item] = resolve;
});
done.push(item);
},
});
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0]);
expect(done).toEqual([]);
waiting[0]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1]);
expect(done).toEqual([0]);
waiting[1]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2]);
waiting[2]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2, 3]);
waiting[3]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2, 3, 4]);
waiting[4]();
await work;
expect(done).toEqual([0, 1, 2, 3, 4]);
});
});
describe('runWorkerQueueThreads', () => {
it('should execute work in parallel', async () => {
const sharedData = new SharedArrayBuffer(10);
const sharedView = new Uint8Array(sharedData);
const results = await runWorkerQueueThreads({
threadCount: 4,
workerData: sharedData,
items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
workerFactory: data => {
const view = new Uint8Array(data);
return async (i: number) => {
view[i] = 10 + i;
return 20 + i;
};
},
});
expect(Array.from(sharedView)).toEqual([
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
]);
expect(results).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29]);
});
});
describe('runWorkerThreads', () => {
it('should run a single thread without items', async () => {
const [result] = await runWorkerThreads({
threadCount: 1,
workerData: 'foo',
worker: async data => `${data}bar`,
});
expect(result).toBe('foobar');
});
it('should run multiple threads without items', async () => {
const results = await runWorkerThreads({
threadCount: 4,
worker: async () => 'foo',
});
expect(results).toEqual(['foo', 'foo', 'foo', 'foo']);
});
it('should send messages', async () => {
const messages = new Array<string>();
await runWorkerThreads({
threadCount: 2,
worker: async (_data, sendMessage) => {
sendMessage('a');
await new Promise(resolve => setTimeout(resolve, 10));
sendMessage('b');
await new Promise(resolve => setTimeout(resolve, 10));
sendMessage('c');
},
onMessage: (message: string) => messages.push(message),
});
expect(messages.sort()).toEqual(['a', 'a', 'b', 'b', 'c', 'c']);
});
});
-342
View File
@@ -1,342 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import os from 'node:os';
import { ErrorLike } from '@backstage/errors';
import { Worker } from 'node:worker_threads';
const defaultParallelism = Math.ceil(os.cpus().length / 2);
const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL';
export type ParallelismOption = boolean | string | number | null | undefined;
export function parseParallelismOption(parallel: ParallelismOption): number {
if (parallel === undefined || parallel === null) {
return defaultParallelism;
} else if (typeof parallel === 'boolean') {
return parallel ? defaultParallelism : 1;
} else if (typeof parallel === 'number' && Number.isInteger(parallel)) {
if (parallel < 1) {
return 1;
}
return parallel;
} else if (typeof parallel === 'string') {
if (parallel === 'true') {
return parseParallelismOption(true);
} else if (parallel === 'false') {
return parseParallelismOption(false);
}
const parsed = Number(parallel);
if (Number.isInteger(parsed)) {
return parseParallelismOption(parsed);
}
}
throw Error(
`Parallel option value '${parallel}' is not a boolean or integer`,
);
}
export function getEnvironmentParallelism() {
return parseParallelismOption(process.env[PARALLEL_ENV_VAR]);
}
type ParallelWorkerOptions<TItem> = {
/**
* Decides the number of parallel workers by multiplying
* this with the configured parallelism, which defaults to 4.
*
* Defaults to 1.
*/
parallelismFactor?: number;
parallelismSetting?: ParallelismOption;
items: Iterable<TItem>;
worker: (item: TItem) => Promise<void>;
};
export async function runParallelWorkers<TItem>(
options: ParallelWorkerOptions<TItem>,
) {
const { parallelismFactor = 1, parallelismSetting, items, worker } = options;
const parallelism = parallelismSetting
? parseParallelismOption(parallelismSetting)
: getEnvironmentParallelism();
const sharedIterator = items[Symbol.iterator]();
const sharedIterable = {
[Symbol.iterator]: () => sharedIterator,
};
const workerCount = Math.max(Math.floor(parallelismFactor * parallelism), 1);
return Promise.all(
Array(workerCount)
.fill(0)
.map(async () => {
for (const value of sharedIterable) {
await worker(value);
}
}),
);
}
type WorkerThreadMessage =
| {
type: 'done';
}
| {
type: 'item';
index: number;
item: unknown;
}
| {
type: 'start';
}
| {
type: 'result';
index: number;
result: unknown;
}
| {
type: 'error';
error: ErrorLike;
}
| {
type: 'message';
message: unknown;
};
export type WorkerQueueThreadsOptions<TItem, TResult, TData> = {
/** The items to process */
items: Iterable<TItem>;
/**
* A function that will be called within each worker thread at startup,
* which should return the worker function that will be called for each item.
*
* This function must be defined as an arrow function or using the
* function keyword, and must be entirely self contained, not referencing
* any variables outside of its scope. This is because the function source
* is stringified and evaluated in the worker thread.
*
* To pass data to the worker, use the `workerData` option and `items`, but
* note that they are both copied by value into the worker thread, except for
* types that are explicitly shareable across threads, such as `SharedArrayBuffer`.
*/
workerFactory: (
data: TData,
) =>
| ((item: TItem) => Promise<TResult>)
| Promise<(item: TItem) => Promise<TResult>>;
/** Data supplied to each worker factory */
workerData?: TData;
/** Number of threads, defaults to half of the number of available CPUs */
threadCount?: number;
};
/**
* Spawns one or more worker threads using the `worker_threads` module.
* Each thread processes one item at a time from the provided `options.items`.
*/
export async function runWorkerQueueThreads<TItem, TResult, TData>(
options: WorkerQueueThreadsOptions<TItem, TResult, TData>,
): Promise<TResult[]> {
const items = Array.from(options.items);
const {
workerFactory,
workerData,
threadCount = Math.min(getEnvironmentParallelism(), items.length),
} = options;
const iterator = items[Symbol.iterator]();
const results = new Array<TResult>();
let itemIndex = 0;
await Promise.all(
Array(threadCount)
.fill(0)
.map(async () => {
const thread = new Worker(`(${workerQueueThread})(${workerFactory})`, {
eval: true,
workerData,
});
return new Promise<void>((resolve, reject) => {
thread.on('message', (message: WorkerThreadMessage) => {
if (message.type === 'start' || message.type === 'result') {
if (message.type === 'result') {
results[message.index] = message.result as TResult;
}
const { value, done } = iterator.next();
if (done) {
thread.postMessage({ type: 'done' });
} else {
thread.postMessage({
type: 'item',
index: itemIndex,
item: value,
});
itemIndex += 1;
}
} else if (message.type === 'error') {
const error = new Error(message.error.message);
error.name = message.error.name;
error.stack = message.error.stack;
reject(error);
}
});
thread.on('error', reject);
thread.on('exit', (code: number) => {
if (code !== 0) {
reject(new Error(`Worker thread exited with code ${code}`));
} else {
resolve();
}
});
});
}),
);
return results;
}
/* istanbul ignore next */
function workerQueueThread(
workerFuncFactory: (
data: unknown,
) => Promise<(item: unknown) => Promise<unknown>>,
) {
const { parentPort, workerData } = require('node:worker_threads');
Promise.resolve()
.then(() => workerFuncFactory(workerData))
.then(
workerFunc => {
parentPort.on('message', async (message: WorkerThreadMessage) => {
if (message.type === 'done') {
parentPort.close();
return;
}
if (message.type === 'item') {
try {
const result = await workerFunc(message.item);
parentPort.postMessage({
type: 'result',
index: message.index,
result,
});
} catch (error) {
parentPort.postMessage({ type: 'error', error });
}
}
});
parentPort.postMessage({ type: 'start' });
},
error => parentPort.postMessage({ type: 'error', error }),
);
}
export type WorkerThreadsOptions<TResult, TData, TMessage> = {
/**
* A function that is called by each worker thread to produce a result.
*
* This function must be defined as an arrow function or using the
* function keyword, and must be entirely self contained, not referencing
* any variables outside of its scope. This is because the function source
* is stringified and evaluated in the worker thread.
*
* To pass data to the worker, use the `workerData` option, but
* note that they are both copied by value into the worker thread, except for
* types that are explicitly shareable across threads, such as `SharedArrayBuffer`.
*/
worker: (
data: TData,
sendMessage: (message: TMessage) => void,
) => Promise<TResult>;
/** Data supplied to each worker */
workerData?: TData;
/** Number of threads, defaults to 1 */
threadCount?: number;
/** An optional handler for messages posted from the worker thread */
onMessage?: (message: TMessage) => void;
};
/**
* Spawns one or more worker threads using the `worker_threads` module.
*/
export async function runWorkerThreads<TResult, TData, TMessage>(
options: WorkerThreadsOptions<TResult, TData, TMessage>,
): Promise<TResult[]> {
const { worker, workerData, threadCount = 1, onMessage } = options;
return Promise.all(
Array(threadCount)
.fill(0)
.map(async () => {
const thread = new Worker(`(${workerThread})(${worker})`, {
eval: true,
workerData,
});
return new Promise<TResult>((resolve, reject) => {
thread.on('message', (message: WorkerThreadMessage) => {
if (message.type === 'result') {
resolve(message.result as TResult);
} else if (message.type === 'error') {
reject(message.error);
} else if (message.type === 'message') {
onMessage?.(message.message as TMessage);
}
});
thread.on('error', reject);
thread.on('exit', (code: number) => {
reject(
new Error(`Unexpected worker thread exit with code ${code}`),
);
});
});
}),
);
}
/* istanbul ignore next */
function workerThread(
workerFunc: (
data: unknown,
sendMessage: (message: unknown) => void,
) => Promise<unknown>,
) {
const { parentPort, workerData } = require('node:worker_threads');
const sendMessage = (message: unknown) => {
parentPort.postMessage({ type: 'message', message });
};
workerFunc(workerData, sendMessage).then(
result => {
parentPort.postMessage({
type: 'result',
index: 0,
result,
});
},
error => {
parentPort.postMessage({ type: 'error', error });
},
);
}
+2 -2
View File
@@ -20,11 +20,11 @@ import {
} from '@backstage/cli-node';
import { resolve as resolvePath } from 'node:path';
import { Project, SourceFile, SyntaxKind, ts, Type } from 'ts-morph';
import { paths } from './paths';
import { targetPaths } from '@backstage/cli-common';
export const createTypeDistProject = async () => {
return new Project({
tsConfigFilePath: paths.resolveTargetRoot('tsconfig.json'),
tsConfigFilePath: targetPaths.resolveRoot('tsconfig.json'),
skipAddingFilesFromTsConfig: true,
});
};
+6 -3
View File
@@ -16,9 +16,12 @@
import fs from 'fs-extra';
import semver from 'semver';
import { paths } from './paths';
import { findOwnPaths } from '@backstage/cli-common';
import { Lockfile } from './versioning';
/* eslint-disable-next-line no-restricted-syntax */
const ownPaths = findOwnPaths(__dirname);
/* eslint-disable @backstage/no-relative-monorepo-imports */
/*
This is a list of all packages used by the templates. If dependencies are added or removed,
@@ -82,12 +85,12 @@ export const packageVersions: Record<string, string> = {
};
export function findVersion() {
const pkgContent = fs.readFileSync(paths.resolveOwn('package.json'), 'utf8');
const pkgContent = fs.readFileSync(ownPaths.resolve('package.json'), 'utf8');
return JSON.parse(pkgContent).version;
}
export const version = findVersion();
export const isDev = fs.pathExistsSync(paths.resolveOwn('src'));
export const isDev = fs.pathExistsSync(ownPaths.resolve('src'));
export function createPackageVersionProvider(
lockfile?: Lockfile,
+2 -8
View File
@@ -15,17 +15,11 @@
*/
import { createMockDirectory } from '@backstage/backend-test-utils';
import { overrideTargetPaths } from '@backstage/cli-common/testUtils';
import { getHasYarnPlugin } from './yarnPlugin';
const mockDir = createMockDirectory();
jest.mock('./paths', () => ({
paths: {
resolveTargetRoot(filename: string) {
return mockDir.resolve(filename);
},
},
}));
overrideTargetPaths(mockDir.path);
describe('getHasYarnPlugin', () => {
beforeEach(() => {
+2 -2
View File
@@ -17,7 +17,7 @@
import fs from 'fs-extra';
import yaml from 'yaml';
import z from 'zod';
import { paths } from './paths';
import { targetPaths } from '@backstage/cli-common';
const yarnRcSchema = z.object({
plugins: z
@@ -35,7 +35,7 @@ const yarnRcSchema = z.object({
* @returns Promise<boolean> - true if the plugin is installed, false otherwise
*/
export async function getHasYarnPlugin(): Promise<boolean> {
const yarnRcPath = paths.resolveTargetRoot('.yarnrc.yml');
const yarnRcPath = targetPaths.resolveRoot('.yarnrc.yml');
const yarnRcContent = await fs.readFile(yarnRcPath, 'utf-8').catch(e => {
if (e.code === 'ENOENT') {
// gracefully continue in case the file doesn't exist
@@ -23,7 +23,8 @@ import {
PackageGraph,
PackageRoles,
} from '@backstage/cli-node';
import { paths } from '../../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { buildFrontend } from '../../../lib/buildFrontend';
import { buildBackend } from '../../../lib/buildBackend';
import { isValidUrl } from '../../../lib/urls';
@@ -41,19 +42,19 @@ export async function command(opts: OptionValues): Promise<void> {
if (isValidUrl(arg)) {
return arg;
}
return paths.resolveTarget(arg);
return targetPaths.resolve(arg);
});
if (role === 'frontend') {
return buildFrontend({
targetDir: paths.targetDir,
targetDir: targetPaths.dir,
configPaths,
writeStats: Boolean(opts.stats),
webpack,
});
}
return buildBackend({
targetDir: paths.targetDir,
targetDir: targetPaths.dir,
configPaths,
skipBuildDependencies: Boolean(opts.skipBuildDependencies),
minify: Boolean(opts.minify),
@@ -76,7 +77,7 @@ export async function command(opts: OptionValues): Promise<void> {
if (isModuleFederationRemote) {
console.log('Building package as a module federation remote');
return buildFrontend({
targetDir: paths.targetDir,
targetDir: targetPaths.dir,
configPaths: [],
writeStats: Boolean(opts.stats),
isModuleFederationRemote,
@@ -99,7 +100,7 @@ export async function command(opts: OptionValues): Promise<void> {
}
const packageJson = (await fs.readJson(
paths.resolveTarget('package.json'),
targetPaths.resolve('package.json'),
)) as BackstagePackageJson;
return buildPackage({
@@ -18,13 +18,13 @@ import { OptionValues } from 'commander';
import { startPackage } from './startPackage';
import { resolveLinkedWorkspace } from './resolveLinkedWorkspace';
import { findRoleFromCommand } from '../../../lib/role';
import { paths } from '../../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
export async function command(opts: OptionValues): Promise<void> {
await startPackage({
role: await findRoleFromCommand(opts),
entrypoint: opts.entrypoint,
targetDir: paths.targetDir,
targetDir: targetPaths.dir,
configPaths: opts.config as string[],
checksEnabled: Boolean(opts.check),
linkedWorkspace: await resolveLinkedWorkspace(opts.link),
@@ -16,7 +16,8 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'node:path';
import { paths } from '../../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { runBackend } from '../../../lib/runner';
interface StartBackendOptions {
@@ -43,7 +44,7 @@ export async function startBackend(options: StartBackendOptions) {
export async function startBackendPlugin(options: StartBackendOptions) {
const hasDevIndexEntry = await fs.pathExists(
resolvePath(options.targetDir ?? paths.targetDir, 'dev/index.ts'),
resolvePath(options.targetDir ?? targetPaths.dir, 'dev/index.ts'),
);
if (!hasDevIndexEntry) {
console.warn(
@@ -20,7 +20,8 @@ import {
getModuleFederationRemoteOptions,
serveBundle,
} from '../../../../build/lib/bundler';
import { paths } from '../../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { BackstagePackageJson } from '@backstage/cli-node';
import { hasReactDomClient } from '../../../../build/lib/bundler/hasReactDomClient';
@@ -38,7 +39,7 @@ interface StartAppOptions {
export async function startFrontend(options: StartAppOptions) {
const packageJson = (await readJson(
resolvePath(options.targetDir ?? paths.targetDir, 'package.json'),
resolvePath(options.targetDir ?? targetPaths.dir, 'package.json'),
)) as BackstagePackageJson;
if (!hasReactDomClient()) {
@@ -58,7 +59,7 @@ export async function startFrontend(options: StartAppOptions) {
moduleFederationRemote: options.isModuleFederationRemote
? await getModuleFederationRemoteOptions(
packageJson,
resolvePath(paths.targetDir),
resolvePath(targetPaths.dir),
)
: undefined,
});
@@ -18,13 +18,14 @@ import chalk from 'chalk';
import { Command, OptionValues } from 'commander';
import { relative as relativePath } from 'node:path';
import { buildPackages, getOutputsForRole } from '../../lib/builder';
import { paths } from '../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import {
BackstagePackage,
PackageGraph,
PackageRoles,
runConcurrentTasks,
} from '@backstage/cli-node';
import { runParallelWorkers } from '../../../../lib/parallel';
import { buildFrontend } from '../../lib/buildFrontend';
import { buildBackend } from '../../lib/buildBackend';
import { createScriptOptionsParser } from '../../../../lib/optionsParser';
@@ -89,7 +90,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
targetDir: pkg.dir,
packageJson: pkg.packageJson,
outputs,
logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `,
logPrefix: `${chalk.cyan(relativePath(targetPaths.rootDir, pkg.dir))}: `,
workspacePackages: packages,
minify: opts.minify ?? buildOptions.minify,
};
@@ -100,9 +101,9 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
if (opts.all) {
console.log('Building apps');
await runParallelWorkers({
await runConcurrentTasks({
items: apps,
parallelismFactor: 1 / 2,
concurrencyFactor: 1 / 2,
worker: async pkg => {
const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build);
if (!buildOptions) {
@@ -121,9 +122,9 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
});
console.log('Building backends');
await runParallelWorkers({
await runConcurrentTasks({
items: backends,
parallelismFactor: 1 / 2,
concurrencyFactor: 1 / 2,
worker: async pkg => {
const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build);
if (!buildOptions) {
@@ -16,8 +16,9 @@
import { PackageGraph } from '@backstage/cli-node';
import { findTargetPackages } from './start';
import { posix } from 'node:path';
import { paths } from '../../../../lib/paths';
import { overrideTargetPaths } from '@backstage/cli-common/testUtils';
overrideTargetPaths('/root');
const mocks = {
app: {
@@ -97,11 +98,6 @@ const mocks = {
describe('findTargetPackages', () => {
beforeEach(() => {
jest.clearAllMocks();
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...parts: string[]) => {
return posix.resolve('/root', ...parts);
});
});
it('should select default packages', async () => {
@@ -20,7 +20,8 @@ import {
PackageRole,
} from '@backstage/cli-node';
import { relative as relativePath } from 'node:path';
import { paths } from '../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { resolveLinkedWorkspace } from '../package/start/resolveLinkedWorkspace';
import { startPackage } from '../package/start/startPackage';
import { parseArgs } from 'node:util';
@@ -95,7 +96,7 @@ export async function findTargetPackages(
pkg => nameOrPath === pkg.packageJson.name,
);
if (!matchingPackage) {
const absPath = paths.resolveTargetRoot(nameOrPath);
const absPath = targetPaths.resolveRoot(nameOrPath);
matchingPackage = packages.find(
pkg => relativePath(pkg.dir, absPath) === '',
);
@@ -117,7 +118,7 @@ export async function findTargetPackages(
);
if (matchingPackages.length > 1) {
// Final fallback is to check for the package path within the monorepo, packages/app or packages/backend
const expectedPath = paths.resolveTargetRoot(
const expectedPath = targetPaths.resolveRoot(
role === 'frontend' ? 'packages/app' : 'packages/backend',
);
const matchByPath = matchingPackages.find(
@@ -19,7 +19,6 @@ import fs from 'fs-extra';
import { resolve as resolvePath } from 'node:path';
import * as tar from 'tar';
import { createDistWorkspace } from './packager';
import { getEnvironmentParallelism } from '../../../lib/parallel';
import { buildPackage, Output } from './builder';
import { PackageGraph } from '@backstage/cli-node';
@@ -53,7 +52,6 @@ export async function buildBackend(options: BuildBackendOptions) {
configPaths,
buildDependencies: !skipBuildDependencies,
buildExcludes: [pkg.name],
parallelism: getEnvironmentParallelism(),
skeleton: SKELETON_FILE,
minify,
});
@@ -17,9 +17,8 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'node:path';
import { buildBundle, getModuleFederationRemoteOptions } from './bundler';
import { getEnvironmentParallelism } from '../../../lib/parallel';
import { loadCliConfig } from '../../config/lib/config';
import { BackstagePackageJson } from '@backstage/cli-node';
import { loadCliConfig } from '../../config/lib/config';
interface BuildAppOptions {
targetDir: string;
@@ -37,7 +36,6 @@ export async function buildFrontend(options: BuildAppOptions) {
await buildBundle({
targetDir,
entry: 'src/index',
parallelism: getEnvironmentParallelism(),
statsJsonEnabled: writeStats,
moduleFederationRemote: options.isModuleFederationRemote
? await getModuleFederationRemoteOptions(
@@ -39,7 +39,8 @@ import {
import { forwardFileImports, cssEntryPoints } from './plugins';
import { BuildOptions, Output } from './types';
import { paths } from '../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { BackstagePackageJson } from '@backstage/cli-node';
import { readEntryPoints } from '../entryPoints';
@@ -116,7 +117,7 @@ export async function makeRollupConfigs(
options: BuildOptions,
): Promise<RollupOptions[]> {
const configs = new Array<RollupOptions>();
const targetDir = options.targetDir ?? paths.targetDir;
const targetDir = options.targetDir ?? targetPaths.dir;
let targetPkg = options.packageJson;
if (!targetPkg) {
@@ -284,9 +285,9 @@ export async function makeRollupConfigs(
const input = Object.fromEntries(
scriptEntryPoints.map(e => [
e.name,
paths.resolveTargetRoot(
targetPaths.resolveRoot(
'dist-types',
relativePath(paths.targetRoot, targetDir),
relativePath(targetPaths.rootDir, targetDir),
e.path.replace(/\.(?:ts|tsx)$/, '.d.ts'),
),
]),
@@ -18,11 +18,11 @@ import fs from 'fs-extra';
import { rollup, RollupOptions } from 'rollup';
import chalk from 'chalk';
import { relative as relativePath, resolve as resolvePath } from 'node:path';
import { paths } from '../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { makeRollupConfigs } from './config';
import { BuildOptions, Output } from './types';
import { PackageRoles } from '@backstage/cli-node';
import { runParallelWorkers } from '../../../../lib/parallel';
import { PackageRoles, runConcurrentTasks } from '@backstage/cli-node';
export function formatErrorMessage(error: any) {
let msg = '';
@@ -34,7 +34,7 @@ export function formatErrorMessage(error: any) {
msg += `\n\n`;
for (const { text, location } of error.errors) {
const { line, column } = location;
const path = relativePath(paths.targetDir, error.id);
const path = relativePath(targetPaths.dir, error.id);
const loc = chalk.cyan(`${path}:${line}:${column}`);
if (text === 'Unexpected "<"' && error.id.endsWith('.js')) {
@@ -53,11 +53,11 @@ export function formatErrorMessage(error: any) {
} else {
// Generic rollup errors, log what's available
if (error.loc) {
const file = `${paths.resolveTarget((error.loc.file || error.id)!)}`;
const file = `${targetPaths.resolve((error.loc.file || error.id)!)}`;
const pos = `${error.loc.line}:${error.loc.column}`;
msg += `${file} [${pos}]\n`;
} else if (error.id) {
msg += `${paths.resolveTarget(error.id)}\n`;
msg += `${targetPaths.resolve(error.id)}\n`;
}
msg += `${error}\n`;
@@ -90,7 +90,7 @@ async function rollupBuild(config: RollupOptions) {
export const buildPackage = async (options: BuildOptions) => {
try {
const { resolutions } = await fs.readJson(
paths.resolveTargetRoot('package.json'),
targetPaths.resolveRoot('package.json'),
);
if (resolutions?.esbuild) {
console.warn(
@@ -107,7 +107,7 @@ export const buildPackage = async (options: BuildOptions) => {
const rollupConfigs = await makeRollupConfigs(options);
const targetDir = options.targetDir ?? paths.targetDir;
const targetDir = options.targetDir ?? targetPaths.dir;
await fs.remove(resolvePath(targetDir, 'dist'));
const buildTasks = rollupConfigs.map(rollupBuild);
@@ -127,7 +127,7 @@ export const buildPackages = async (options: BuildOptions[]) => {
const buildTasks = rollupConfigs.flat().map(opts => () => rollupBuild(opts));
await runParallelWorkers({
await runConcurrentTasks({
items: buildTasks,
worker: async task => task(),
});
@@ -25,11 +25,12 @@ import { TsCheckerRspackPlugin } from 'ts-checker-rspack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
import { paths as cliPaths } from '../../../../lib/paths';
import fs from 'fs-extra';
import { optimization as optimizationConfig } from './optimization';
import pickBy from 'lodash/pickBy';
import { runOutput } from '@backstage/cli-common';
import { runOutput, targetPaths } from '@backstage/cli-common';
import { transforms } from './transforms';
import { version } from '../../../../lib/version';
import yn from 'yn';
@@ -96,7 +97,7 @@ async function readBuildInfo() {
}
const { version: packageVersion } = await fs.readJson(
cliPaths.resolveTarget('package.json'),
targetPaths.resolve('package.json'),
);
return {
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import { paths } from '../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
export function hasReactDomClient() {
try {
require.resolve('react-dom/client', {
paths: [paths.targetDir],
paths: [targetPaths.dir],
});
return true;
} catch {
@@ -17,7 +17,7 @@
import { relative as relativePath } from 'node:path';
import { getPackages } from '@manypkg/get-packages';
import { rspack } from '@rspack/core';
import { paths } from '../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
/**
* This returns of collection of plugins that links a separate workspace into
@@ -52,7 +52,7 @@ export async function createWorkspaceLinkingPlugins(
/^react(?:-router)?(?:-dom)?$/,
resource => {
if (!relativePath(linkedRoot.dir, resource.context).startsWith('..')) {
resource.context = paths.targetDir;
resource.context = targetPaths.dir;
}
},
),
@@ -28,7 +28,7 @@ import {
HostSharedDependencies,
RuntimeSharedDependenciesGlobal,
} from '@backstage/module-federation-common';
import { dirname, join as joinPath, resolve as resolvePath } from 'path';
import { dirname, join as joinPath, resolve as resolvePath } from 'node:path';
import fs from 'fs-extra';
import chokidar from 'chokidar';
import PQueue from 'p-queue';
@@ -20,7 +20,7 @@ import chokidar from 'chokidar';
import fs from 'fs-extra';
import PQueue from 'p-queue';
import { dirname, join as joinPath, resolve as resolvePath } from 'node:path';
import { paths as cliPaths } from '../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
const DETECTED_MODULES_MODULE_NAME = '__backstage-autodetected-plugins__';
@@ -146,7 +146,7 @@ export async function createDetectedModulesEntryPoint(options: {
// Previous versions of the CLI would write the detected modules file to the
// root `node_modules`, this makes sure that doesn't exist to minimize risk of conflicts
const legacyDetectedModulesPath = joinPath(
cliPaths.targetRoot,
targetPaths.rootDir,
'node_modules',
`${DETECTED_MODULES_MODULE_NAME}.js`,
);

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