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:
@@ -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,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"
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-app-legacy",
|
||||
"version": "0.2.118",
|
||||
"version": "0.2.119-next.0",
|
||||
"backstage": {
|
||||
"role": "frontend"
|
||||
},
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"version": "0.0.32",
|
||||
"version": "0.0.33-next.0",
|
||||
"backstage": {
|
||||
"role": "frontend"
|
||||
},
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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
@@ -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,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,
|
||||
|
||||
+117
@@ -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);
|
||||
}
|
||||
}
|
||||
+2
-6
@@ -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';
|
||||
+130
@@ -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,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"
|
||||
|
||||
@@ -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,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',
|
||||
});
|
||||
|
||||
@@ -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,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(),
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-backend",
|
||||
"version": "0.0.47",
|
||||
"version": "0.0.48-next.0",
|
||||
"backstage": {
|
||||
"role": "backend"
|
||||
},
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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;
|
||||
```
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user