From 06c2015e5b57c2518123c65ad13138d1c5875a79 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Feb 2026 16:19:49 +0100 Subject: [PATCH 1/8] Move parallel worker utilities to @backstage/cli-node Moves `runParallelWorkers`, `runWorkerQueueThreads`, `runWorkerThreads`, `parseParallelismOption`, and `getEnvironmentParallelism` from the CLI internal lib to the shared `@backstage/cli-node` package. This is part of the ongoing effort to make CLI modules independent of each other and the shared lib code. Signed-off-by: Patrik Oldsberg --- .changeset/cli-node-parallel-helpers.md | 5 ++ packages/cli-node/report.api.md | 55 +++++++++++++++++++ packages/cli-node/src/index.ts | 1 + packages/cli-node/src/parallel/index.ts | 28 ++++++++++ .../src/parallel}/parallel.test.ts | 0 .../lib => cli-node/src/parallel}/parallel.ts | 44 ++++++++++++++- .../src/modules/build/commands/repo/build.ts | 2 +- .../cli/src/modules/build/lib/buildBackend.ts | 3 +- .../src/modules/build/lib/buildFrontend.ts | 6 +- .../src/modules/build/lib/builder/packager.ts | 3 +- .../build/lib/packager/createDistWorkspace.ts | 2 +- .../src/modules/lint/commands/repo/lint.ts | 2 +- .../modules/migrate/commands/versions/bump.ts | 2 +- 13 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 .changeset/cli-node-parallel-helpers.md create mode 100644 packages/cli-node/src/parallel/index.ts rename packages/{cli/src/lib => cli-node/src/parallel}/parallel.test.ts (100%) rename packages/{cli/src/lib => cli-node/src/parallel}/parallel.ts (92%) diff --git a/.changeset/cli-node-parallel-helpers.md b/.changeset/cli-node-parallel-helpers.md new file mode 100644 index 0000000000..d4cea5c3c5 --- /dev/null +++ b/.changeset/cli-node-parallel-helpers.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': minor +--- + +Added parallel worker utilities: `runParallelWorkers`, `runWorkerQueueThreads`, `runWorkerThreads`, `parseParallelismOption`, and `getEnvironmentParallelism`. These were moved from the `@backstage/cli` internal code. diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 55d2443643..960bbf9f59 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -86,6 +86,9 @@ export interface BackstagePackageJson { version: string; } +// @public +export function getEnvironmentParallelism(): number; + // @public export class GitUtils { static listChangedFiles(ref: string): Promise; @@ -200,4 +203,56 @@ export class PackageRoles { static getRoleFromPackage(pkgJson: unknown): PackageRole | undefined; static getRoleInfo(role: string): PackageRoleInfo; } + +// @public +export type ParallelismOption = boolean | string | number | null | undefined; + +// @public +export type ParallelWorkerOptions = { + parallelismFactor?: number; + parallelismSetting?: ParallelismOption; + items: Iterable; + worker: (item: TItem) => Promise; +}; + +// @public +export function parseParallelismOption(parallel: ParallelismOption): number; + +// @public +export function runParallelWorkers( + options: ParallelWorkerOptions, +): Promise; + +// @public +export function runWorkerQueueThreads( + options: WorkerQueueThreadsOptions, +): Promise; + +// @public +export function runWorkerThreads( + options: WorkerThreadsOptions, +): Promise; + +// @public +export type WorkerQueueThreadsOptions = { + items: Iterable; + workerFactory: ( + data: TData, + ) => + | ((item: TItem) => Promise) + | Promise<(item: TItem) => Promise>; + workerData?: TData; + threadCount?: number; +}; + +// @public +export type WorkerThreadsOptions = { + worker: ( + data: TData, + sendMessage: (message: TMessage) => void, + ) => Promise; + workerData?: TData; + threadCount?: number; + onMessage?: (message: TMessage) => void; +}; ``` diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index 1d3f3ec2ed..6d1c519b9e 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -22,4 +22,5 @@ export * from './git'; export * from './monorepo'; +export * from './parallel'; export * from './roles'; diff --git a/packages/cli-node/src/parallel/index.ts b/packages/cli-node/src/parallel/index.ts new file mode 100644 index 0000000000..60cd2e28b2 --- /dev/null +++ b/packages/cli-node/src/parallel/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { ParallelismOption, ParallelWorkerOptions } from './parallel'; +export { + parseParallelismOption, + getEnvironmentParallelism, + runParallelWorkers, + runWorkerQueueThreads, + runWorkerThreads, +} from './parallel'; +export type { + WorkerQueueThreadsOptions, + WorkerThreadsOptions, +} from './parallel'; diff --git a/packages/cli/src/lib/parallel.test.ts b/packages/cli-node/src/parallel/parallel.test.ts similarity index 100% rename from packages/cli/src/lib/parallel.test.ts rename to packages/cli-node/src/parallel/parallel.test.ts diff --git a/packages/cli/src/lib/parallel.ts b/packages/cli-node/src/parallel/parallel.ts similarity index 92% rename from packages/cli/src/lib/parallel.ts rename to packages/cli-node/src/parallel/parallel.ts index a4ca5dd13d..26c5803a93 100644 --- a/packages/cli/src/lib/parallel.ts +++ b/packages/cli-node/src/parallel/parallel.ts @@ -22,8 +22,21 @@ const defaultParallelism = Math.ceil(os.cpus().length / 2); const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; +/** + * Options for configuring parallelism. Can be a boolean, string, number, null, or undefined. + * - Boolean: true uses default parallelism (half of CPUs), false uses 1 + * - Number: explicit worker count + * - String: parsed as boolean or integer (e.g. "true", "4") + * + * @public + */ export type ParallelismOption = boolean | string | number | null | undefined; +/** + * Parses a parallelism option value into a concrete worker count. + * + * @public + */ export function parseParallelismOption(parallel: ParallelismOption): number { if (parallel === undefined || parallel === null) { return defaultParallelism; @@ -51,11 +64,21 @@ export function parseParallelismOption(parallel: ParallelismOption): number { ); } +/** + * Returns the parallelism value from the BACKSTAGE_CLI_BUILD_PARALLEL environment variable. + * + * @public + */ export function getEnvironmentParallelism() { return parseParallelismOption(process.env[PARALLEL_ENV_VAR]); } -type ParallelWorkerOptions = { +/** + * Options for runParallelWorkers. + * + * @public + */ +export type ParallelWorkerOptions = { /** * Decides the number of parallel workers by multiplying * this with the configured parallelism, which defaults to 4. @@ -68,6 +91,11 @@ type ParallelWorkerOptions = { worker: (item: TItem) => Promise; }; +/** + * Runs items through a worker function in parallel across multiple async workers. + * + * @public + */ export async function runParallelWorkers( options: ParallelWorkerOptions, ) { @@ -119,6 +147,11 @@ type WorkerThreadMessage = message: unknown; }; +/** + * Options for runWorkerQueueThreads. + * + * @public + */ export type WorkerQueueThreadsOptions = { /** The items to process */ items: Iterable; @@ -149,6 +182,8 @@ export type WorkerQueueThreadsOptions = { /** * Spawns one or more worker threads using the `worker_threads` module. * Each thread processes one item at a time from the provided `options.items`. + * + * @public */ export async function runWorkerQueueThreads( options: WorkerQueueThreadsOptions, @@ -250,6 +285,11 @@ function workerQueueThread( ); } +/** + * Options for runWorkerThreads. + * + * @public + */ export type WorkerThreadsOptions = { /** * A function that is called by each worker thread to produce a result. @@ -277,6 +317,8 @@ export type WorkerThreadsOptions = { /** * Spawns one or more worker threads using the `worker_threads` module. + * + * @public */ export async function runWorkerThreads( options: WorkerThreadsOptions, diff --git a/packages/cli/src/modules/build/commands/repo/build.ts b/packages/cli/src/modules/build/commands/repo/build.ts index e64fe9eff0..22c72c91dd 100644 --- a/packages/cli/src/modules/build/commands/repo/build.ts +++ b/packages/cli/src/modules/build/commands/repo/build.ts @@ -23,8 +23,8 @@ import { BackstagePackage, PackageGraph, PackageRoles, + runParallelWorkers, } from '@backstage/cli-node'; -import { runParallelWorkers } from '../../../../lib/parallel'; import { buildFrontend } from '../../lib/buildFrontend'; import { buildBackend } from '../../lib/buildBackend'; import { createScriptOptionsParser } from '../../../../lib/optionsParser'; diff --git a/packages/cli/src/modules/build/lib/buildBackend.ts b/packages/cli/src/modules/build/lib/buildBackend.ts index f377d068bd..ddf9b1f667 100644 --- a/packages/cli/src/modules/build/lib/buildBackend.ts +++ b/packages/cli/src/modules/build/lib/buildBackend.ts @@ -19,9 +19,8 @@ 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'; +import { PackageGraph, getEnvironmentParallelism } from '@backstage/cli-node'; const BUNDLE_FILE = 'bundle.tar.gz'; const SKELETON_FILE = 'skeleton.tar.gz'; diff --git a/packages/cli/src/modules/build/lib/buildFrontend.ts b/packages/cli/src/modules/build/lib/buildFrontend.ts index 7b0955a519..bb00de7951 100644 --- a/packages/cli/src/modules/build/lib/buildFrontend.ts +++ b/packages/cli/src/modules/build/lib/buildFrontend.ts @@ -17,9 +17,11 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { buildBundle, getModuleFederationRemoteOptions } from './bundler'; -import { getEnvironmentParallelism } from '../../../lib/parallel'; +import { + BackstagePackageJson, + getEnvironmentParallelism, +} from '@backstage/cli-node'; import { loadCliConfig } from '../../config/lib/config'; -import { BackstagePackageJson } from '@backstage/cli-node'; interface BuildAppOptions { targetDir: string; diff --git a/packages/cli/src/modules/build/lib/builder/packager.ts b/packages/cli/src/modules/build/lib/builder/packager.ts index 12017b694c..b0a34780d7 100644 --- a/packages/cli/src/modules/build/lib/builder/packager.ts +++ b/packages/cli/src/modules/build/lib/builder/packager.ts @@ -21,8 +21,7 @@ import { relative as relativePath, resolve as resolvePath } from 'node:path'; import { paths } from '../../../../lib/paths'; import { makeRollupConfigs } from './config'; import { BuildOptions, Output } from './types'; -import { PackageRoles } from '@backstage/cli-node'; -import { runParallelWorkers } from '../../../../lib/parallel'; +import { PackageRoles, runParallelWorkers } from '@backstage/cli-node'; export function formatErrorMessage(error: any) { let msg = ''; diff --git a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts b/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts index 0f096a3127..b53bca62a6 100644 --- a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts +++ b/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts @@ -41,8 +41,8 @@ import { PackageRoles, PackageGraph, PackageGraphNode, + runParallelWorkers, } from '@backstage/cli-node'; -import { runParallelWorkers } from '../../../../lib/parallel'; import { createTypeDistProject } from '../../../../lib/typeDistProject'; // These packages aren't safe to pack in parallel since the CLI depends on them diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index 458ccf1a63..0e17f97dd2 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -23,9 +23,9 @@ import { PackageGraph, BackstagePackageJson, Lockfile, + runWorkerQueueThreads, } from '@backstage/cli-node'; import { paths } from '../../../../lib/paths'; -import { runWorkerQueueThreads } from '../../../../lib/parallel'; import { createScriptOptionsParser } from '../../../../lib/optionsParser'; import { SuccessCache } from '../../../../lib/cache/SuccessCache'; diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index ab65f8c604..06658cea5f 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -33,7 +33,7 @@ import { mapDependencies, YarnInfoInspectData, } from '../../../../lib/versioning'; -import { runParallelWorkers } from '../../../../lib/parallel'; +import { runParallelWorkers } from '@backstage/cli-node'; import { getManifestByReleaseLine, getManifestByVersion, From 649d3ca3b6481c4981b8b04bb74bfafb18b0c439 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Feb 2026 16:49:55 +0100 Subject: [PATCH 2/8] Remove dead parallelism options and narrow public API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parallelism fields on bundler types (BundlingOptions, BuildOptions, BackendBundlingOptions) and createDistWorkspace Options were defined but never read — fully dead code. Likewise the parallelismSetting option on ParallelWorkerOptions was never passed by any caller. Also narrows the cli-node public API to only export the three runner functions and their option types, keeping getEnvironmentParallelism and parseParallelismOption as internal utilities. Signed-off-by: Patrik Oldsberg --- .changeset/cli-node-parallel-helpers.md | 2 +- packages/cli-node/report.api.md | 10 -------- packages/cli-node/src/parallel/index.ts | 4 +-- .../cli-node/src/parallel/parallel.test.ts | 6 ++++- packages/cli-node/src/parallel/parallel.ts | 25 ++----------------- .../cli/src/modules/build/lib/buildBackend.ts | 3 +-- .../src/modules/build/lib/buildFrontend.ts | 6 +---- .../src/modules/build/lib/bundler/types.ts | 3 --- .../build/lib/packager/createDistWorkspace.ts | 5 ---- 9 files changed, 11 insertions(+), 53 deletions(-) diff --git a/.changeset/cli-node-parallel-helpers.md b/.changeset/cli-node-parallel-helpers.md index d4cea5c3c5..d34f063a27 100644 --- a/.changeset/cli-node-parallel-helpers.md +++ b/.changeset/cli-node-parallel-helpers.md @@ -2,4 +2,4 @@ '@backstage/cli-node': minor --- -Added parallel worker utilities: `runParallelWorkers`, `runWorkerQueueThreads`, `runWorkerThreads`, `parseParallelismOption`, and `getEnvironmentParallelism`. These were moved from the `@backstage/cli` internal code. +Added parallel worker utilities: `runParallelWorkers`, `runWorkerQueueThreads`, and `runWorkerThreads`. These were moved from the `@backstage/cli` internal code. diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 960bbf9f59..67085b815f 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -86,9 +86,6 @@ export interface BackstagePackageJson { version: string; } -// @public -export function getEnvironmentParallelism(): number; - // @public export class GitUtils { static listChangedFiles(ref: string): Promise; @@ -204,20 +201,13 @@ export class PackageRoles { static getRoleInfo(role: string): PackageRoleInfo; } -// @public -export type ParallelismOption = boolean | string | number | null | undefined; - // @public export type ParallelWorkerOptions = { parallelismFactor?: number; - parallelismSetting?: ParallelismOption; items: Iterable; worker: (item: TItem) => Promise; }; -// @public -export function parseParallelismOption(parallel: ParallelismOption): number; - // @public export function runParallelWorkers( options: ParallelWorkerOptions, diff --git a/packages/cli-node/src/parallel/index.ts b/packages/cli-node/src/parallel/index.ts index 60cd2e28b2..0b7c46712d 100644 --- a/packages/cli-node/src/parallel/index.ts +++ b/packages/cli-node/src/parallel/index.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -export type { ParallelismOption, ParallelWorkerOptions } from './parallel'; +export type { ParallelWorkerOptions } from './parallel'; export { - parseParallelismOption, - getEnvironmentParallelism, runParallelWorkers, runWorkerQueueThreads, runWorkerThreads, diff --git a/packages/cli-node/src/parallel/parallel.test.ts b/packages/cli-node/src/parallel/parallel.test.ts index 389e22102b..51a09377ed 100644 --- a/packages/cli-node/src/parallel/parallel.test.ts +++ b/packages/cli-node/src/parallel/parallel.test.ts @@ -66,14 +66,18 @@ describe('getEnvironmentParallelism', () => { }); describe('runParallelWorkers', () => { + afterEach(() => { + delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL; + }); + it('executes work in parallel', async () => { const started = new Array(); const done = new Array(); const waiting = new Array<() => void>(); + process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '4'; const work = runParallelWorkers({ items: [0, 1, 2, 3, 4], - parallelismSetting: 4, parallelismFactor: 0.5, // 2 at a time worker: async item => { started.push(item); diff --git a/packages/cli-node/src/parallel/parallel.ts b/packages/cli-node/src/parallel/parallel.ts index 26c5803a93..1ec960adb0 100644 --- a/packages/cli-node/src/parallel/parallel.ts +++ b/packages/cli-node/src/parallel/parallel.ts @@ -22,21 +22,8 @@ const defaultParallelism = Math.ceil(os.cpus().length / 2); const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; -/** - * Options for configuring parallelism. Can be a boolean, string, number, null, or undefined. - * - Boolean: true uses default parallelism (half of CPUs), false uses 1 - * - Number: explicit worker count - * - String: parsed as boolean or integer (e.g. "true", "4") - * - * @public - */ export type ParallelismOption = boolean | string | number | null | undefined; -/** - * Parses a parallelism option value into a concrete worker count. - * - * @public - */ export function parseParallelismOption(parallel: ParallelismOption): number { if (parallel === undefined || parallel === null) { return defaultParallelism; @@ -64,11 +51,6 @@ export function parseParallelismOption(parallel: ParallelismOption): number { ); } -/** - * Returns the parallelism value from the BACKSTAGE_CLI_BUILD_PARALLEL environment variable. - * - * @public - */ export function getEnvironmentParallelism() { return parseParallelismOption(process.env[PARALLEL_ENV_VAR]); } @@ -86,7 +68,6 @@ export type ParallelWorkerOptions = { * Defaults to 1. */ parallelismFactor?: number; - parallelismSetting?: ParallelismOption; items: Iterable; worker: (item: TItem) => Promise; }; @@ -99,10 +80,8 @@ export type ParallelWorkerOptions = { export async function runParallelWorkers( options: ParallelWorkerOptions, ) { - const { parallelismFactor = 1, parallelismSetting, items, worker } = options; - const parallelism = parallelismSetting - ? parseParallelismOption(parallelismSetting) - : getEnvironmentParallelism(); + const { parallelismFactor = 1, items, worker } = options; + const parallelism = getEnvironmentParallelism(); const sharedIterator = items[Symbol.iterator](); const sharedIterable = { diff --git a/packages/cli/src/modules/build/lib/buildBackend.ts b/packages/cli/src/modules/build/lib/buildBackend.ts index ddf9b1f667..bf2ebf16ba 100644 --- a/packages/cli/src/modules/build/lib/buildBackend.ts +++ b/packages/cli/src/modules/build/lib/buildBackend.ts @@ -20,7 +20,7 @@ import { resolve as resolvePath } from 'node:path'; import * as tar from 'tar'; import { createDistWorkspace } from './packager'; import { buildPackage, Output } from './builder'; -import { PackageGraph, getEnvironmentParallelism } from '@backstage/cli-node'; +import { PackageGraph } from '@backstage/cli-node'; const BUNDLE_FILE = 'bundle.tar.gz'; const SKELETON_FILE = 'skeleton.tar.gz'; @@ -52,7 +52,6 @@ export async function buildBackend(options: BuildBackendOptions) { configPaths, buildDependencies: !skipBuildDependencies, buildExcludes: [pkg.name], - parallelism: getEnvironmentParallelism(), skeleton: SKELETON_FILE, minify, }); diff --git a/packages/cli/src/modules/build/lib/buildFrontend.ts b/packages/cli/src/modules/build/lib/buildFrontend.ts index bb00de7951..6a5785a5b9 100644 --- a/packages/cli/src/modules/build/lib/buildFrontend.ts +++ b/packages/cli/src/modules/build/lib/buildFrontend.ts @@ -17,10 +17,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { buildBundle, getModuleFederationRemoteOptions } from './bundler'; -import { - BackstagePackageJson, - getEnvironmentParallelism, -} from '@backstage/cli-node'; +import { BackstagePackageJson } from '@backstage/cli-node'; import { loadCliConfig } from '../../config/lib/config'; interface BuildAppOptions { @@ -39,7 +36,6 @@ export async function buildFrontend(options: BuildAppOptions) { await buildBundle({ targetDir, entry: 'src/index', - parallelism: getEnvironmentParallelism(), statsJsonEnabled: writeStats, moduleFederationRemote: options.isModuleFederationRemote ? await getModuleFederationRemoteOptions( diff --git a/packages/cli/src/modules/build/lib/bundler/types.ts b/packages/cli/src/modules/build/lib/bundler/types.ts index a9a4b0f51f..d8960bc03e 100644 --- a/packages/cli/src/modules/build/lib/bundler/types.ts +++ b/packages/cli/src/modules/build/lib/bundler/types.ts @@ -36,7 +36,6 @@ export type BundlingOptions = { isDev: boolean; frontendConfig: Config; getFrontendAppConfigs(): AppConfig[]; - parallelism?: number; additionalEntryPoints?: string[]; // Path to append to the detected public path, e.g. '/public' publicSubPath?: string; @@ -63,7 +62,6 @@ export type BuildOptions = BundlingPathsOptions & { // Target directory, defaulting to paths.targetDir targetDir?: string; statsJsonEnabled: boolean; - parallelism?: number; schema?: ConfigSchema; frontendConfig: Config; frontendAppConfigs: AppConfig[]; @@ -75,7 +73,6 @@ export type BuildOptions = BundlingPathsOptions & { export type BackendBundlingOptions = { checksEnabled: boolean; isDev: boolean; - parallelism?: number; inspectEnabled: boolean; inspectBrkEnabled: boolean; require?: string; diff --git a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts b/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts index b53bca62a6..f0bbaa4ff1 100644 --- a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts +++ b/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts @@ -86,11 +86,6 @@ type Options = { */ buildExcludes?: string[]; - /** - * Controls amount of parallelism in some build steps. - */ - parallelism?: number; - /** * If set, creates a skeleton tarball that contains all package.json files * with the same structure as the workspace dir. From c8237e212d5122d567f0791c7460f8d46c4d78e1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Feb 2026 17:20:16 +0100 Subject: [PATCH 3/8] Rename parallelism env var to BACKSTAGE_CLI_PARALLELISM Renames from BACKSTAGE_CLI_BUILD_PARALLEL to the more general BACKSTAGE_CLI_PARALLELISM. The old name is still supported but logs a one-time deprecation warning. Signed-off-by: Patrik Oldsberg --- .../cli-node/src/parallel/parallel.test.ts | 39 ++++++++++++++++--- packages/cli-node/src/parallel/parallel.ts | 19 ++++++++- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/packages/cli-node/src/parallel/parallel.test.ts b/packages/cli-node/src/parallel/parallel.test.ts index 51a09377ed..2ab713d9aa 100644 --- a/packages/cli-node/src/parallel/parallel.test.ts +++ b/packages/cli-node/src/parallel/parallel.test.ts @@ -50,24 +50,51 @@ describe('parseParallelismOption', () => { }); describe('getEnvironmentParallelism', () => { + afterEach(() => { + delete process.env.BACKSTAGE_CLI_PARALLELISM; + delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL; + }); + it('reads the parallelism setting from the environment', () => { - process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '2'; + process.env.BACKSTAGE_CLI_PARALLELISM = '2'; expect(getEnvironmentParallelism()).toBe(2); - process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'true'; + process.env.BACKSTAGE_CLI_PARALLELISM = 'true'; expect(getEnvironmentParallelism()).toBe(defaultParallelism); - process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'false'; + process.env.BACKSTAGE_CLI_PARALLELISM = 'false'; expect(getEnvironmentParallelism()).toBe(1); - delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL; + delete process.env.BACKSTAGE_CLI_PARALLELISM; expect(getEnvironmentParallelism()).toBe(defaultParallelism); }); + + it('supports the deprecated BACKSTAGE_CLI_BUILD_PARALLEL with a warning', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '3'; + expect(getEnvironmentParallelism()).toBe(3); + expect(warnSpy).toHaveBeenCalledWith( + 'The BACKSTAGE_CLI_BUILD_PARALLEL environment variable is deprecated, use BACKSTAGE_CLI_PARALLELISM instead', + ); + + warnSpy.mockClear(); + expect(getEnvironmentParallelism()).toBe(3); + expect(warnSpy).not.toHaveBeenCalled(); + + warnSpy.mockRestore(); + }); + + it('prefers BACKSTAGE_CLI_PARALLELISM over the deprecated variable', () => { + process.env.BACKSTAGE_CLI_PARALLELISM = '5'; + process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '3'; + expect(getEnvironmentParallelism()).toBe(5); + }); }); describe('runParallelWorkers', () => { afterEach(() => { - delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL; + delete process.env.BACKSTAGE_CLI_PARALLELISM; }); it('executes work in parallel', async () => { @@ -75,7 +102,7 @@ describe('runParallelWorkers', () => { const done = new Array(); const waiting = new Array<() => void>(); - process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '4'; + process.env.BACKSTAGE_CLI_PARALLELISM = '4'; const work = runParallelWorkers({ items: [0, 1, 2, 3, 4], parallelismFactor: 0.5, // 2 at a time diff --git a/packages/cli-node/src/parallel/parallel.ts b/packages/cli-node/src/parallel/parallel.ts index 1ec960adb0..91a39980bd 100644 --- a/packages/cli-node/src/parallel/parallel.ts +++ b/packages/cli-node/src/parallel/parallel.ts @@ -20,7 +20,8 @@ import { Worker } from 'node:worker_threads'; const defaultParallelism = Math.ceil(os.cpus().length / 2); -const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; +const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_PARALLELISM'; +const DEPRECATED_PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; export type ParallelismOption = boolean | string | number | null | undefined; @@ -51,8 +52,22 @@ export function parseParallelismOption(parallel: ParallelismOption): number { ); } +let hasWarnedDeprecation = false; + export function getEnvironmentParallelism() { - return parseParallelismOption(process.env[PARALLEL_ENV_VAR]); + if (process.env[PARALLEL_ENV_VAR] !== undefined) { + return parseParallelismOption(process.env[PARALLEL_ENV_VAR]); + } + if (process.env[DEPRECATED_PARALLEL_ENV_VAR] !== undefined) { + if (!hasWarnedDeprecation) { + hasWarnedDeprecation = true; + console.warn( + `The ${DEPRECATED_PARALLEL_ENV_VAR} environment variable is deprecated, use ${PARALLEL_ENV_VAR} instead`, + ); + } + return parseParallelismOption(process.env[DEPRECATED_PARALLEL_ENV_VAR]); + } + return defaultParallelism; } /** From 4f0c7ec86a72058a2cea1ac2e88ae52d0e324b32 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 23 Feb 2026 00:41:17 +0100 Subject: [PATCH 4/8] Address PR review feedback - Rename runParallelWorkers to runConcurrentTasks, return void - Rename ParallelWorkerOptions to ConcurrentTasksOptions - Rename parallelismFactor to concurrencyFactor - Remove unused runWorkerThreads and WorkerThreadsOptions - Rename workerData to context in WorkerQueueThreadsOptions - Drop threadCount from public API types - Rename env var to BACKSTAGE_CLI_CONCURRENCY - Make cli-node changeset a patch Signed-off-by: Patrik Oldsberg Co-authored-by: Cursor --- .changeset/cli-node-parallel-helpers.md | 4 +- packages/cli-node/report.api.md | 47 ++-- packages/cli-node/src/parallel/index.ts | 12 +- .../cli-node/src/parallel/parallel.test.ts | 175 ++++----------- packages/cli-node/src/parallel/parallel.ts | 209 +++++------------- .../src/modules/build/commands/repo/build.ts | 10 +- .../src/modules/build/lib/builder/packager.ts | 4 +- .../build/lib/packager/createDistWorkspace.ts | 6 +- .../src/modules/lint/commands/repo/lint.ts | 2 +- .../modules/migrate/commands/versions/bump.ts | 10 +- 10 files changed, 140 insertions(+), 339 deletions(-) diff --git a/.changeset/cli-node-parallel-helpers.md b/.changeset/cli-node-parallel-helpers.md index d34f063a27..5ad7ad7f4d 100644 --- a/.changeset/cli-node-parallel-helpers.md +++ b/.changeset/cli-node-parallel-helpers.md @@ -1,5 +1,5 @@ --- -'@backstage/cli-node': minor +'@backstage/cli-node': patch --- -Added parallel worker utilities: `runParallelWorkers`, `runWorkerQueueThreads`, and `runWorkerThreads`. These were moved from the `@backstage/cli` internal code. +Added `runConcurrentTasks` and `runWorkerQueueThreads` utilities, moved from the `@backstage/cli` internal code. diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 67085b815f..902eff2b91 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -86,6 +86,13 @@ export interface BackstagePackageJson { version: string; } +// @public +export type ConcurrentTasksOptions = { + concurrencyFactor?: number; + items: Iterable; + worker: (item: TItem) => Promise; +}; + // @public export class GitUtils { static listChangedFiles(ref: string): Promise; @@ -202,47 +209,23 @@ export class PackageRoles { } // @public -export type ParallelWorkerOptions = { - parallelismFactor?: number; - items: Iterable; - worker: (item: TItem) => Promise; -}; +export function runConcurrentTasks( + options: ConcurrentTasksOptions, +): Promise; // @public -export function runParallelWorkers( - options: ParallelWorkerOptions, -): Promise; - -// @public -export function runWorkerQueueThreads( - options: WorkerQueueThreadsOptions, +export function runWorkerQueueThreads( + options: WorkerQueueThreadsOptions, ): Promise; // @public -export function runWorkerThreads( - options: WorkerThreadsOptions, -): Promise; - -// @public -export type WorkerQueueThreadsOptions = { +export type WorkerQueueThreadsOptions = { items: Iterable; workerFactory: ( - data: TData, + context: TContext, ) => | ((item: TItem) => Promise) | Promise<(item: TItem) => Promise>; - workerData?: TData; - threadCount?: number; -}; - -// @public -export type WorkerThreadsOptions = { - worker: ( - data: TData, - sendMessage: (message: TMessage) => void, - ) => Promise; - workerData?: TData; - threadCount?: number; - onMessage?: (message: TMessage) => void; + context?: TContext; }; ``` diff --git a/packages/cli-node/src/parallel/index.ts b/packages/cli-node/src/parallel/index.ts index 0b7c46712d..7aadbafe70 100644 --- a/packages/cli-node/src/parallel/index.ts +++ b/packages/cli-node/src/parallel/index.ts @@ -14,13 +14,5 @@ * limitations under the License. */ -export type { ParallelWorkerOptions } from './parallel'; -export { - runParallelWorkers, - runWorkerQueueThreads, - runWorkerThreads, -} from './parallel'; -export type { - WorkerQueueThreadsOptions, - WorkerThreadsOptions, -} from './parallel'; +export type { ConcurrentTasksOptions, WorkerQueueThreadsOptions } from './parallel'; +export { runConcurrentTasks, runWorkerQueueThreads } from './parallel'; diff --git a/packages/cli-node/src/parallel/parallel.test.ts b/packages/cli-node/src/parallel/parallel.test.ts index 2ab713d9aa..b0d3be3769 100644 --- a/packages/cli-node/src/parallel/parallel.test.ts +++ b/packages/cli-node/src/parallel/parallel.test.ts @@ -14,87 +14,11 @@ * limitations under the License. */ -import os from 'node:os'; -import { - parseParallelismOption, - getEnvironmentParallelism, - runParallelWorkers, - runWorkerQueueThreads, - runWorkerThreads, -} from './parallel'; +import { runConcurrentTasks, runWorkerQueueThreads } 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', () => { +describe('runConcurrentTasks', () => { afterEach(() => { - delete process.env.BACKSTAGE_CLI_PARALLELISM; - delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL; - }); - - it('reads the parallelism setting from the environment', () => { - process.env.BACKSTAGE_CLI_PARALLELISM = '2'; - expect(getEnvironmentParallelism()).toBe(2); - - process.env.BACKSTAGE_CLI_PARALLELISM = 'true'; - expect(getEnvironmentParallelism()).toBe(defaultParallelism); - - process.env.BACKSTAGE_CLI_PARALLELISM = 'false'; - expect(getEnvironmentParallelism()).toBe(1); - - delete process.env.BACKSTAGE_CLI_PARALLELISM; - expect(getEnvironmentParallelism()).toBe(defaultParallelism); - }); - - it('supports the deprecated BACKSTAGE_CLI_BUILD_PARALLEL with a warning', () => { - const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); - - process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '3'; - expect(getEnvironmentParallelism()).toBe(3); - expect(warnSpy).toHaveBeenCalledWith( - 'The BACKSTAGE_CLI_BUILD_PARALLEL environment variable is deprecated, use BACKSTAGE_CLI_PARALLELISM instead', - ); - - warnSpy.mockClear(); - expect(getEnvironmentParallelism()).toBe(3); - expect(warnSpy).not.toHaveBeenCalled(); - - warnSpy.mockRestore(); - }); - - it('prefers BACKSTAGE_CLI_PARALLELISM over the deprecated variable', () => { - process.env.BACKSTAGE_CLI_PARALLELISM = '5'; - process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '3'; - expect(getEnvironmentParallelism()).toBe(5); - }); -}); - -describe('runParallelWorkers', () => { - afterEach(() => { - delete process.env.BACKSTAGE_CLI_PARALLELISM; + delete process.env.BACKSTAGE_CLI_CONCURRENCY; }); it('executes work in parallel', async () => { @@ -102,10 +26,10 @@ describe('runParallelWorkers', () => { const done = new Array(); const waiting = new Array<() => void>(); - process.env.BACKSTAGE_CLI_PARALLELISM = '4'; - const work = runParallelWorkers({ + process.env.BACKSTAGE_CLI_CONCURRENCY = '4'; + const work = runConcurrentTasks({ items: [0, 1, 2, 3, 4], - parallelismFactor: 0.5, // 2 at a time + concurrencyFactor: 0.5, // 2 at a time worker: async item => { started.push(item); await new Promise(resolve => { @@ -141,9 +65,9 @@ describe('runParallelWorkers', () => { const done = new Array(); const waiting = new Array<() => void>(); - const work = runParallelWorkers({ + const work = runConcurrentTasks({ items: [0, 1, 2, 3, 4], - parallelismFactor: 0, // 1 at a time + concurrencyFactor: 0, // 1 at a time worker: async item => { started.push(item); await new Promise(resolve => { @@ -178,6 +102,45 @@ describe('runParallelWorkers', () => { await work; expect(done).toEqual([0, 1, 2, 3, 4]); }); + + it('returns void', async () => { + const result = await runConcurrentTasks({ + items: [1, 2, 3], + worker: async () => {}, + }); + expect(result).toBeUndefined(); + }); + + it('defaults to environment concurrency', async () => { + const started = new Array(); + process.env.BACKSTAGE_CLI_CONCURRENCY = '2'; + + await runConcurrentTasks({ + items: [0, 1], + worker: async item => { + started.push(item); + }, + }); + + expect(started).toEqual([0, 1]); + }); + + it('supports the deprecated BACKSTAGE_CLI_BUILD_PARALLEL with a warning', async () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '2'; + await runConcurrentTasks({ + items: [0, 1], + worker: async () => {}, + }); + + expect(warnSpy).toHaveBeenCalledWith( + 'The BACKSTAGE_CLI_BUILD_PARALLEL environment variable is deprecated, use BACKSTAGE_CLI_CONCURRENCY instead', + ); + + delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL; + warnSpy.mockRestore(); + }); }); describe('runWorkerQueueThreads', () => { @@ -186,10 +149,9 @@ describe('runWorkerQueueThreads', () => { const sharedView = new Uint8Array(sharedData); const results = await runWorkerQueueThreads({ - threadCount: 4, - workerData: sharedData, + context: sharedData, items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - workerFactory: data => { + workerFactory: (data: SharedArrayBuffer) => { const view = new Uint8Array(data); return async (i: number) => { @@ -205,42 +167,3 @@ describe('runWorkerQueueThreads', () => { expect(results).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29]); }); }); - -describe('runWorkerThreads', () => { - it('should run a single thread without items', async () => { - const [result] = await runWorkerThreads({ - threadCount: 1, - workerData: 'foo', - worker: async data => `${data}bar`, - }); - - expect(result).toBe('foobar'); - }); - - it('should run multiple threads without items', async () => { - const results = await runWorkerThreads({ - threadCount: 4, - worker: async () => 'foo', - }); - - expect(results).toEqual(['foo', 'foo', 'foo', 'foo']); - }); - - it('should send messages', async () => { - const messages = new Array(); - - await runWorkerThreads({ - threadCount: 2, - worker: async (_data, sendMessage) => { - sendMessage('a'); - await new Promise(resolve => setTimeout(resolve, 10)); - sendMessage('b'); - await new Promise(resolve => setTimeout(resolve, 10)); - sendMessage('c'); - }, - onMessage: (message: string) => messages.push(message), - }); - - expect(messages.sort()).toEqual(['a', 'a', 'b', 'b', 'c', 'c']); - }); -}); diff --git a/packages/cli-node/src/parallel/parallel.ts b/packages/cli-node/src/parallel/parallel.ts index 91a39980bd..f925ed91e4 100644 --- a/packages/cli-node/src/parallel/parallel.ts +++ b/packages/cli-node/src/parallel/parallel.ts @@ -18,93 +18,98 @@ 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 defaultConcurrency = Math.max(Math.ceil(os.cpus().length / 2), 1); -const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_PARALLELISM'; -const DEPRECATED_PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; +const CONCURRENCY_ENV_VAR = 'BACKSTAGE_CLI_CONCURRENCY'; +const DEPRECATED_CONCURRENCY_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; -export type ParallelismOption = boolean | string | number | null | undefined; +type ConcurrencyOption = 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) { +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 parallel; - } else if (typeof parallel === 'string') { - if (parallel === 'true') { - return parseParallelismOption(true); - } else if (parallel === 'false') { - return parseParallelismOption(false); + return value; + } else if (typeof value === 'string') { + if (value === 'true') { + return parseConcurrencyOption(true); + } else if (value === 'false') { + return parseConcurrencyOption(false); } - const parsed = Number(parallel); + const parsed = Number(value); if (Number.isInteger(parsed)) { - return parseParallelismOption(parsed); + return parseConcurrencyOption(parsed); } } throw Error( - `Parallel option value '${parallel}' is not a boolean or integer`, + `Concurrency option value '${value}' is not a boolean or integer`, ); } let hasWarnedDeprecation = false; -export function getEnvironmentParallelism() { - if (process.env[PARALLEL_ENV_VAR] !== undefined) { - return parseParallelismOption(process.env[PARALLEL_ENV_VAR]); +function getEnvironmentConcurrency() { + if (process.env[CONCURRENCY_ENV_VAR] !== undefined) { + return parseConcurrencyOption(process.env[CONCURRENCY_ENV_VAR]); } - if (process.env[DEPRECATED_PARALLEL_ENV_VAR] !== undefined) { + if (process.env[DEPRECATED_CONCURRENCY_ENV_VAR] !== undefined) { if (!hasWarnedDeprecation) { hasWarnedDeprecation = true; console.warn( - `The ${DEPRECATED_PARALLEL_ENV_VAR} environment variable is deprecated, use ${PARALLEL_ENV_VAR} instead`, + `The ${DEPRECATED_CONCURRENCY_ENV_VAR} environment variable is deprecated, use ${CONCURRENCY_ENV_VAR} instead`, ); } - return parseParallelismOption(process.env[DEPRECATED_PARALLEL_ENV_VAR]); + return parseConcurrencyOption( + process.env[DEPRECATED_CONCURRENCY_ENV_VAR], + ); } - return defaultParallelism; + return defaultConcurrency; } /** - * Options for runParallelWorkers. + * Options for {@link runConcurrentTasks}. * * @public */ -export type ParallelWorkerOptions = { +export type ConcurrentTasksOptions = { /** - * Decides the number of parallel workers by multiplying - * this with the configured parallelism, which defaults to 4. + * Decides the number of concurrent workers by multiplying + * this with the configured concurrency. * * Defaults to 1. */ - parallelismFactor?: number; + concurrencyFactor?: number; items: Iterable; worker: (item: TItem) => Promise; }; /** - * Runs items through a worker function in parallel across multiple async workers. + * Runs items through a worker function concurrently across multiple async workers. * * @public */ -export async function runParallelWorkers( - options: ParallelWorkerOptions, -) { - const { parallelismFactor = 1, items, worker } = options; - const parallelism = getEnvironmentParallelism(); +export async function runConcurrentTasks( + options: ConcurrentTasksOptions, +): Promise { + const { concurrencyFactor = 1, items, worker } = options; + const concurrency = getEnvironmentConcurrency(); const sharedIterator = items[Symbol.iterator](); const sharedIterable = { [Symbol.iterator]: () => sharedIterator, }; - const workerCount = Math.max(Math.floor(parallelismFactor * parallelism), 1); - return Promise.all( + const workerCount = Math.max( + Math.floor(concurrencyFactor * concurrency), + 1, + ); + await Promise.all( Array(workerCount) .fill(0) .map(async () => { @@ -142,11 +147,11 @@ type WorkerThreadMessage = }; /** - * Options for runWorkerQueueThreads. + * Options for {@link runWorkerQueueThreads}. * * @public */ -export type WorkerQueueThreadsOptions = { +export type WorkerQueueThreadsOptions = { /** The items to process */ items: Iterable; /** @@ -158,19 +163,17 @@ export type WorkerQueueThreadsOptions = { * 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 + * 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: ( - data: TData, + context: TContext, ) => | ((item: TItem) => Promise) | Promise<(item: TItem) => Promise>; - /** Data supplied to each worker factory */ - workerData?: TData; - /** Number of threads, defaults to half of the number of available CPUs */ - threadCount?: number; + /** Context data supplied to each worker factory */ + context?: TContext; }; /** @@ -179,15 +182,13 @@ export type WorkerQueueThreadsOptions = { * * @public */ -export async function runWorkerQueueThreads( - options: WorkerQueueThreadsOptions, +export async function runWorkerQueueThreads( + options: WorkerQueueThreadsOptions, ): Promise { const items = Array.from(options.items); - const { - workerFactory, - workerData, - threadCount = Math.min(getEnvironmentParallelism(), items.length), - } = options; + const workerFactory = options.workerFactory; + const workerData = options.context; + const threadCount = Math.min(getEnvironmentConcurrency(), items.length); const iterator = items[Symbol.iterator](); const results = new Array(); @@ -278,101 +279,3 @@ function workerQueueThread( error => parentPort.postMessage({ type: 'error', error }), ); } - -/** - * Options for runWorkerThreads. - * - * @public - */ -export type WorkerThreadsOptions = { - /** - * A function that is called by each worker thread to produce a result. - * - * This function must be defined as an arrow function or using the - * function keyword, and must be entirely self contained, not referencing - * any variables outside of its scope. This is because the function source - * is stringified and evaluated in the worker thread. - * - * To pass data to the worker, use the `workerData` option, but - * note that they are both copied by value into the worker thread, except for - * types that are explicitly shareable across threads, such as `SharedArrayBuffer`. - */ - worker: ( - data: TData, - sendMessage: (message: TMessage) => void, - ) => Promise; - /** Data supplied to each worker */ - workerData?: TData; - /** Number of threads, defaults to 1 */ - threadCount?: number; - /** An optional handler for messages posted from the worker thread */ - onMessage?: (message: TMessage) => void; -}; - -/** - * Spawns one or more worker threads using the `worker_threads` module. - * - * @public - */ -export async function runWorkerThreads( - options: WorkerThreadsOptions, -): Promise { - const { worker, workerData, threadCount = 1, onMessage } = options; - - return Promise.all( - Array(threadCount) - .fill(0) - .map(async () => { - const thread = new Worker(`(${workerThread})(${worker})`, { - eval: true, - workerData, - }); - - return new Promise((resolve, reject) => { - thread.on('message', (message: WorkerThreadMessage) => { - if (message.type === 'result') { - resolve(message.result as TResult); - } else if (message.type === 'error') { - reject(message.error); - } else if (message.type === 'message') { - onMessage?.(message.message as TMessage); - } - }); - - thread.on('error', reject); - thread.on('exit', (code: number) => { - reject( - new Error(`Unexpected worker thread exit with code ${code}`), - ); - }); - }); - }), - ); -} - -/* istanbul ignore next */ -function workerThread( - workerFunc: ( - data: unknown, - sendMessage: (message: unknown) => void, - ) => Promise, -) { - const { parentPort, workerData } = require('node:worker_threads'); - - const sendMessage = (message: unknown) => { - parentPort.postMessage({ type: 'message', message }); - }; - - workerFunc(workerData, sendMessage).then( - result => { - parentPort.postMessage({ - type: 'result', - index: 0, - result, - }); - }, - error => { - parentPort.postMessage({ type: 'error', error }); - }, - ); -} diff --git a/packages/cli/src/modules/build/commands/repo/build.ts b/packages/cli/src/modules/build/commands/repo/build.ts index 22c72c91dd..6c99ebb0af 100644 --- a/packages/cli/src/modules/build/commands/repo/build.ts +++ b/packages/cli/src/modules/build/commands/repo/build.ts @@ -23,7 +23,7 @@ import { BackstagePackage, PackageGraph, PackageRoles, - runParallelWorkers, + runConcurrentTasks, } from '@backstage/cli-node'; import { buildFrontend } from '../../lib/buildFrontend'; import { buildBackend } from '../../lib/buildBackend'; @@ -100,9 +100,9 @@ export async function command(opts: OptionValues, cmd: Command): Promise { 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 +121,9 @@ export async function command(opts: OptionValues, cmd: Command): Promise { }); console.log('Building backends'); - await runParallelWorkers({ + await runConcurrentTasks({ items: backends, - parallelismFactor: 1 / 2, + concurrencyFactor: 1 / 2, worker: async pkg => { const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build); if (!buildOptions) { diff --git a/packages/cli/src/modules/build/lib/builder/packager.ts b/packages/cli/src/modules/build/lib/builder/packager.ts index b0a34780d7..77b1e2b134 100644 --- a/packages/cli/src/modules/build/lib/builder/packager.ts +++ b/packages/cli/src/modules/build/lib/builder/packager.ts @@ -21,7 +21,7 @@ import { relative as relativePath, resolve as resolvePath } from 'node:path'; import { paths } from '../../../../lib/paths'; import { makeRollupConfigs } from './config'; import { BuildOptions, Output } from './types'; -import { PackageRoles, runParallelWorkers } from '@backstage/cli-node'; +import { PackageRoles, runConcurrentTasks } from '@backstage/cli-node'; export function formatErrorMessage(error: any) { let msg = ''; @@ -126,7 +126,7 @@ export const buildPackages = async (options: BuildOptions[]) => { const buildTasks = rollupConfigs.flat().map(opts => () => rollupBuild(opts)); - await runParallelWorkers({ + await runConcurrentTasks({ items: buildTasks, worker: async task => task(), }); diff --git a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts b/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts index f0bbaa4ff1..296355e2dd 100644 --- a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts +++ b/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts @@ -41,7 +41,7 @@ import { PackageRoles, PackageGraph, PackageGraphNode, - runParallelWorkers, + runConcurrentTasks, } from '@backstage/cli-node'; import { createTypeDistProject } from '../../../../lib/typeDistProject'; @@ -220,7 +220,7 @@ export async function createDistWorkspace( await buildPackages(standardBuilds); if (customBuild.length > 0) { - await runParallelWorkers({ + await runConcurrentTasks({ items: customBuild, worker: async ({ name, dir, args }) => { await run(['yarn', 'run', 'build', ...(args || [])], { @@ -363,7 +363,7 @@ async function moveToDistWorkspace( } // Repacking in parallel is much faster and safe for all packages outside of the Backstage repo - await runParallelWorkers({ + await runConcurrentTasks({ items: safePackages.map((target, index) => ({ target, index })), worker: async ({ target, index }) => { await pack(target, `temp-package-${index}.tgz`); diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index 0e17f97dd2..07e4eac3ff 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -107,7 +107,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { const resultsList = await runWorkerQueueThreads({ items: items.filter(item => item.lintOptions), // Filter out packages without lint script - workerData: { + context: { fix: Boolean(opts.fix), format: opts.format as string | undefined, shouldCache: Boolean(cacheContext), diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 06658cea5f..a0a73dd78e 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -33,7 +33,7 @@ import { mapDependencies, YarnInfoInspectData, } from '../../../../lib/versioning'; -import { runParallelWorkers } from '@backstage/cli-node'; +import { runConcurrentTasks } from '@backstage/cli-node'; import { getManifestByReleaseLine, getManifestByVersion, @@ -145,8 +145,8 @@ export default async (opts: OptionValues) => { // Next check with the package registry to see which dependency ranges we need to bump const versionBumps = new Map(); - await runParallelWorkers({ - parallelismFactor: 4, + await runConcurrentTasks({ + concurrencyFactor: 4, items: dependencyMap.entries(), async worker([name, pkgs]) { let target: string; @@ -182,8 +182,8 @@ export default async (opts: OptionValues) => { console.log(); const breakingUpdates = new Map(); - await runParallelWorkers({ - parallelismFactor: 4, + await runConcurrentTasks({ + concurrencyFactor: 4, items: versionBumps.entries(), async worker([name, deps]) { const pkgPath = resolvePath(deps[0].location, 'package.json'); From 67958e2d6c37adde53d37801c051bfa071d90658 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 23 Feb 2026 10:59:43 +0100 Subject: [PATCH 5/8] Fix prettier formatting Signed-off-by: Patrik Oldsberg Co-authored-by: Cursor --- packages/cli-node/src/parallel/index.ts | 5 ++++- packages/cli-node/src/parallel/parallel.ts | 9 ++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/cli-node/src/parallel/index.ts b/packages/cli-node/src/parallel/index.ts index 7aadbafe70..f8f9d4d51f 100644 --- a/packages/cli-node/src/parallel/index.ts +++ b/packages/cli-node/src/parallel/index.ts @@ -14,5 +14,8 @@ * limitations under the License. */ -export type { ConcurrentTasksOptions, WorkerQueueThreadsOptions } from './parallel'; +export type { + ConcurrentTasksOptions, + WorkerQueueThreadsOptions, +} from './parallel'; export { runConcurrentTasks, runWorkerQueueThreads } from './parallel'; diff --git a/packages/cli-node/src/parallel/parallel.ts b/packages/cli-node/src/parallel/parallel.ts index f925ed91e4..9d36852351 100644 --- a/packages/cli-node/src/parallel/parallel.ts +++ b/packages/cli-node/src/parallel/parallel.ts @@ -65,9 +65,7 @@ function getEnvironmentConcurrency() { `The ${DEPRECATED_CONCURRENCY_ENV_VAR} environment variable is deprecated, use ${CONCURRENCY_ENV_VAR} instead`, ); } - return parseConcurrencyOption( - process.env[DEPRECATED_CONCURRENCY_ENV_VAR], - ); + return parseConcurrencyOption(process.env[DEPRECATED_CONCURRENCY_ENV_VAR]); } return defaultConcurrency; } @@ -105,10 +103,7 @@ export async function runConcurrentTasks( [Symbol.iterator]: () => sharedIterator, }; - const workerCount = Math.max( - Math.floor(concurrencyFactor * concurrency), - 1, - ); + const workerCount = Math.max(Math.floor(concurrencyFactor * concurrency), 1); await Promise.all( Array(workerCount) .fill(0) From 90ffb545528a9c0c718450e977b3a0a1a19fff23 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 23 Feb 2026 11:21:51 +0100 Subject: [PATCH 6/8] Rename parallel/ to concurrency/ and split into separate files Signed-off-by: Patrik Oldsberg Co-authored-by: Cursor --- .../cli-node/src/concurrency/concurrency.ts | 70 ++++++++++++ .../src/{parallel => concurrency}/index.ts | 9 +- .../runConcurrentTasks.test.ts} | 27 +---- .../src/concurrency/runConcurrentTasks.ts | 62 +++++++++++ .../concurrency/runWorkerQueueThreads.test.ts | 42 +++++++ .../runWorkerQueueThreads.ts} | 103 +----------------- packages/cli-node/src/index.ts | 2 +- 7 files changed, 181 insertions(+), 134 deletions(-) create mode 100644 packages/cli-node/src/concurrency/concurrency.ts rename packages/cli-node/src/{parallel => concurrency}/index.ts (69%) rename packages/cli-node/src/{parallel/parallel.test.ts => concurrency/runConcurrentTasks.test.ts} (83%) create mode 100644 packages/cli-node/src/concurrency/runConcurrentTasks.ts create mode 100644 packages/cli-node/src/concurrency/runWorkerQueueThreads.test.ts rename packages/cli-node/src/{parallel/parallel.ts => concurrency/runWorkerQueueThreads.ts} (65%) diff --git a/packages/cli-node/src/concurrency/concurrency.ts b/packages/cli-node/src/concurrency/concurrency.ts new file mode 100644 index 0000000000..fa46f132a0 --- /dev/null +++ b/packages/cli-node/src/concurrency/concurrency.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import os from 'node:os'; + +const defaultConcurrency = Math.max(Math.ceil(os.cpus().length / 2), 1); + +const CONCURRENCY_ENV_VAR = 'BACKSTAGE_CLI_CONCURRENCY'; +const DEPRECATED_CONCURRENCY_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; + +type ConcurrencyOption = boolean | string | number | null | undefined; + +function parseConcurrencyOption(value: ConcurrencyOption): number { + if (value === undefined || value === null) { + return defaultConcurrency; + } else if (typeof value === 'boolean') { + return value ? defaultConcurrency : 1; + } else if (typeof value === 'number' && Number.isInteger(value)) { + if (value < 1) { + return 1; + } + return value; + } else if (typeof value === 'string') { + if (value === 'true') { + return parseConcurrencyOption(true); + } else if (value === 'false') { + return parseConcurrencyOption(false); + } + const parsed = Number(value); + if (Number.isInteger(parsed)) { + return parseConcurrencyOption(parsed); + } + } + + throw Error( + `Concurrency option value '${value}' is not a boolean or integer`, + ); +} + +let hasWarnedDeprecation = false; + +/** @internal */ +export function getEnvironmentConcurrency() { + if (process.env[CONCURRENCY_ENV_VAR] !== undefined) { + return parseConcurrencyOption(process.env[CONCURRENCY_ENV_VAR]); + } + if (process.env[DEPRECATED_CONCURRENCY_ENV_VAR] !== undefined) { + if (!hasWarnedDeprecation) { + hasWarnedDeprecation = true; + console.warn( + `The ${DEPRECATED_CONCURRENCY_ENV_VAR} environment variable is deprecated, use ${CONCURRENCY_ENV_VAR} instead`, + ); + } + return parseConcurrencyOption(process.env[DEPRECATED_CONCURRENCY_ENV_VAR]); + } + return defaultConcurrency; +} diff --git a/packages/cli-node/src/parallel/index.ts b/packages/cli-node/src/concurrency/index.ts similarity index 69% rename from packages/cli-node/src/parallel/index.ts rename to packages/cli-node/src/concurrency/index.ts index f8f9d4d51f..3ff4067a20 100644 --- a/packages/cli-node/src/parallel/index.ts +++ b/packages/cli-node/src/concurrency/index.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -export type { - ConcurrentTasksOptions, - WorkerQueueThreadsOptions, -} from './parallel'; -export { runConcurrentTasks, runWorkerQueueThreads } from './parallel'; +export type { ConcurrentTasksOptions } from './runConcurrentTasks'; +export { runConcurrentTasks } from './runConcurrentTasks'; +export type { WorkerQueueThreadsOptions } from './runWorkerQueueThreads'; +export { runWorkerQueueThreads } from './runWorkerQueueThreads'; diff --git a/packages/cli-node/src/parallel/parallel.test.ts b/packages/cli-node/src/concurrency/runConcurrentTasks.test.ts similarity index 83% rename from packages/cli-node/src/parallel/parallel.test.ts rename to packages/cli-node/src/concurrency/runConcurrentTasks.test.ts index b0d3be3769..382a85d297 100644 --- a/packages/cli-node/src/parallel/parallel.test.ts +++ b/packages/cli-node/src/concurrency/runConcurrentTasks.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { runConcurrentTasks, runWorkerQueueThreads } from './parallel'; +import { runConcurrentTasks } from './runConcurrentTasks'; describe('runConcurrentTasks', () => { afterEach(() => { @@ -142,28 +142,3 @@ describe('runConcurrentTasks', () => { warnSpy.mockRestore(); }); }); - -describe('runWorkerQueueThreads', () => { - it('should execute work in parallel', async () => { - const sharedData = new SharedArrayBuffer(10); - const sharedView = new Uint8Array(sharedData); - - const results = await runWorkerQueueThreads({ - context: sharedData, - items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - workerFactory: (data: SharedArrayBuffer) => { - const view = new Uint8Array(data); - - return async (i: number) => { - view[i] = 10 + i; - return 20 + i; - }; - }, - }); - - expect(Array.from(sharedView)).toEqual([ - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, - ]); - expect(results).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29]); - }); -}); diff --git a/packages/cli-node/src/concurrency/runConcurrentTasks.ts b/packages/cli-node/src/concurrency/runConcurrentTasks.ts new file mode 100644 index 0000000000..1695f25403 --- /dev/null +++ b/packages/cli-node/src/concurrency/runConcurrentTasks.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getEnvironmentConcurrency } from './concurrency'; + +/** + * Options for {@link runConcurrentTasks}. + * + * @public + */ +export type ConcurrentTasksOptions = { + /** + * Decides the number of concurrent workers by multiplying + * this with the configured concurrency. + * + * Defaults to 1. + */ + concurrencyFactor?: number; + items: Iterable; + worker: (item: TItem) => Promise; +}; + +/** + * Runs items through a worker function concurrently across multiple async workers. + * + * @public + */ +export async function runConcurrentTasks( + options: ConcurrentTasksOptions, +): Promise { + const { concurrencyFactor = 1, items, worker } = options; + const concurrency = getEnvironmentConcurrency(); + + const sharedIterator = items[Symbol.iterator](); + const sharedIterable = { + [Symbol.iterator]: () => sharedIterator, + }; + + const workerCount = Math.max(Math.floor(concurrencyFactor * concurrency), 1); + await Promise.all( + Array(workerCount) + .fill(0) + .map(async () => { + for (const value of sharedIterable) { + await worker(value); + } + }), + ); +} diff --git a/packages/cli-node/src/concurrency/runWorkerQueueThreads.test.ts b/packages/cli-node/src/concurrency/runWorkerQueueThreads.test.ts new file mode 100644 index 0000000000..20e71531ab --- /dev/null +++ b/packages/cli-node/src/concurrency/runWorkerQueueThreads.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { runWorkerQueueThreads } from './runWorkerQueueThreads'; + +describe('runWorkerQueueThreads', () => { + it('should execute work in parallel', async () => { + const sharedData = new SharedArrayBuffer(10); + const sharedView = new Uint8Array(sharedData); + + const results = await runWorkerQueueThreads({ + context: sharedData, + items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + workerFactory: (data: SharedArrayBuffer) => { + const view = new Uint8Array(data); + + return async (i: number) => { + view[i] = 10 + i; + return 20 + i; + }; + }, + }); + + expect(Array.from(sharedView)).toEqual([ + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + ]); + expect(results).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29]); + }); +}); diff --git a/packages/cli-node/src/parallel/parallel.ts b/packages/cli-node/src/concurrency/runWorkerQueueThreads.ts similarity index 65% rename from packages/cli-node/src/parallel/parallel.ts rename to packages/cli-node/src/concurrency/runWorkerQueueThreads.ts index 9d36852351..d1974dba90 100644 --- a/packages/cli-node/src/parallel/parallel.ts +++ b/packages/cli-node/src/concurrency/runWorkerQueueThreads.ts @@ -14,106 +14,9 @@ * limitations under the License. */ -import os from 'node:os'; import { ErrorLike } from '@backstage/errors'; import { Worker } from 'node:worker_threads'; - -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; - -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; -} - -/** - * Options for {@link runConcurrentTasks}. - * - * @public - */ -export type ConcurrentTasksOptions = { - /** - * Decides the number of concurrent workers by multiplying - * this with the configured concurrency. - * - * Defaults to 1. - */ - concurrencyFactor?: number; - items: Iterable; - worker: (item: TItem) => Promise; -}; - -/** - * Runs items through a worker function concurrently across multiple async workers. - * - * @public - */ -export async function runConcurrentTasks( - options: ConcurrentTasksOptions, -): Promise { - const { concurrencyFactor = 1, items, worker } = options; - const concurrency = getEnvironmentConcurrency(); - - const sharedIterator = items[Symbol.iterator](); - const sharedIterable = { - [Symbol.iterator]: () => sharedIterator, - }; - - const workerCount = Math.max(Math.floor(concurrencyFactor * concurrency), 1); - await Promise.all( - Array(workerCount) - .fill(0) - .map(async () => { - for (const value of sharedIterable) { - await worker(value); - } - }), - ); -} +import { getEnvironmentConcurrency } from './concurrency'; type WorkerThreadMessage = | { @@ -135,10 +38,6 @@ type WorkerThreadMessage = | { type: 'error'; error: ErrorLike; - } - | { - type: 'message'; - message: unknown; }; /** diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index 6d1c519b9e..5540666a05 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -22,5 +22,5 @@ export * from './git'; export * from './monorepo'; -export * from './parallel'; +export * from './concurrency'; export * from './roles'; From 951924d0c26372c370862e87cc5e3195f7543783 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 23 Feb 2026 11:25:17 +0100 Subject: [PATCH 7/8] Update CLI changeset message Signed-off-by: Patrik Oldsberg Co-authored-by: Cursor --- .changeset/cli-internal-refactor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/cli-internal-refactor.md b/.changeset/cli-internal-refactor.md index 97957a2cd2..c3f8f38540 100644 --- a/.changeset/cli-internal-refactor.md +++ b/.changeset/cli-internal-refactor.md @@ -2,4 +2,4 @@ '@backstage/cli': patch --- -Internal refactor to improve module independence. +Internal refactor to use new concurrency utilities from `@backstage/cli-node`. From f467a4126e7a549f1eb4fad6ce2268f7e2670597 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 23 Feb 2026 11:38:23 +0100 Subject: [PATCH 8/8] Return object from runWorkerQueueThreads Signed-off-by: Patrik Oldsberg Co-authored-by: Cursor --- packages/cli-node/report.api.md | 4 +++- .../cli-node/src/concurrency/runWorkerQueueThreads.test.ts | 2 +- packages/cli-node/src/concurrency/runWorkerQueueThreads.ts | 4 ++-- packages/cli/src/modules/lint/commands/repo/lint.ts | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 902eff2b91..b3d8fe5641 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -216,7 +216,9 @@ export function runConcurrentTasks( // @public export function runWorkerQueueThreads( options: WorkerQueueThreadsOptions, -): Promise; +): Promise<{ + results: TResult[]; +}>; // @public export type WorkerQueueThreadsOptions = { diff --git a/packages/cli-node/src/concurrency/runWorkerQueueThreads.test.ts b/packages/cli-node/src/concurrency/runWorkerQueueThreads.test.ts index 20e71531ab..5dd2cda438 100644 --- a/packages/cli-node/src/concurrency/runWorkerQueueThreads.test.ts +++ b/packages/cli-node/src/concurrency/runWorkerQueueThreads.test.ts @@ -21,7 +21,7 @@ describe('runWorkerQueueThreads', () => { const sharedData = new SharedArrayBuffer(10); const sharedView = new Uint8Array(sharedData); - const results = await runWorkerQueueThreads({ + const { results } = await runWorkerQueueThreads({ context: sharedData, items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], workerFactory: (data: SharedArrayBuffer) => { diff --git a/packages/cli-node/src/concurrency/runWorkerQueueThreads.ts b/packages/cli-node/src/concurrency/runWorkerQueueThreads.ts index d1974dba90..2d2a3b37e4 100644 --- a/packages/cli-node/src/concurrency/runWorkerQueueThreads.ts +++ b/packages/cli-node/src/concurrency/runWorkerQueueThreads.ts @@ -78,7 +78,7 @@ export type WorkerQueueThreadsOptions = { */ export async function runWorkerQueueThreads( options: WorkerQueueThreadsOptions, -): Promise { +): Promise<{ results: TResult[] }> { const items = Array.from(options.items); const workerFactory = options.workerFactory; const workerData = options.context; @@ -134,7 +134,7 @@ export async function runWorkerQueueThreads( }), ); - return results; + return { results }; } /* istanbul ignore next */ diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index 07e4eac3ff..98a100fe3e 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -105,7 +105,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { }), ); - const resultsList = await runWorkerQueueThreads({ + const { results: resultsList } = await runWorkerQueueThreads({ items: items.filter(item => item.lintOptions), // Filter out packages without lint script context: { fix: Boolean(opts.fix),