From 3c811bf8a960ab5ec0f1cfde1277e9df0420bb3e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 25 Feb 2026 11:52:36 +0100 Subject: [PATCH 1/4] cli-node, cli: move yarnPlugin and SuccessCache to cli-node Move `getHasYarnPlugin` and `SuccessCache` from `@backstage/cli` internal modules to `@backstage/cli-node` as public exports, making them available for reuse by other CLI tooling. Signed-off-by: Patrik Oldsberg --- .changeset/cli-use-cli-node-utils.md | 5 +++++ .changeset/move-utils-to-cli-node.md | 5 +++++ packages/cli-node/package.json | 1 + packages/cli-node/report.api.md | 13 +++++++++++++ .../lib => cli-node/src}/cache/SuccessCache.ts | 7 ++++++- packages/cli-node/src/cache/index.ts | 17 +++++++++++++++++ packages/cli-node/src/index.ts | 4 +++- packages/cli-node/src/yarn/index.ts | 17 +++++++++++++++++ .../src/yarn}/yarnPlugin.test.ts | 0 .../src/lib => cli-node/src/yarn}/yarnPlugin.ts | 2 +- .../cli/src/modules/lint/commands/repo/lint.ts | 2 +- .../modules/migrate/commands/versions/bump.ts | 7 +++++-- .../new/lib/execution/PortableTemplater.ts | 2 +- .../cli/src/modules/test/commands/repo/test.ts | 2 +- yarn.lock | 1 + 15 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 .changeset/cli-use-cli-node-utils.md create mode 100644 .changeset/move-utils-to-cli-node.md rename packages/{cli/src/lib => cli-node/src}/cache/SuccessCache.ts (95%) create mode 100644 packages/cli-node/src/cache/index.ts create mode 100644 packages/cli-node/src/yarn/index.ts rename packages/{cli/src/lib => cli-node/src/yarn}/yarnPlugin.test.ts (100%) rename packages/{cli/src/lib => cli-node/src/yarn}/yarnPlugin.ts (96%) diff --git a/.changeset/cli-use-cli-node-utils.md b/.changeset/cli-use-cli-node-utils.md new file mode 100644 index 0000000000..6b1d6be92d --- /dev/null +++ b/.changeset/cli-use-cli-node-utils.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Migrated `getHasYarnPlugin` and `SuccessCache` to use the implementations from `@backstage/cli-node`. diff --git a/.changeset/move-utils-to-cli-node.md b/.changeset/move-utils-to-cli-node.md new file mode 100644 index 0000000000..cf91e0b54f --- /dev/null +++ b/.changeset/move-utils-to-cli-node.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': patch +--- + +Added `getHasYarnPlugin` and `SuccessCache` exports, moved from `@backstage/cli`. diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index 014ed56992..16cfba3c0d 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -39,6 +39,7 @@ "@yarnpkg/parsers": "^3.0.0", "fs-extra": "^11.2.0", "semver": "^7.5.3", + "yaml": "^2.0.0", "zod": "^3.25.76" }, "devDependencies": { diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index cf27d92894..372bf34206 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -93,6 +93,9 @@ export type ConcurrentTasksOptions = { worker: (item: TItem) => Promise; }; +// @public +export function getHasYarnPlugin(): Promise; + // @public export class GitUtils { static listChangedFiles(ref: string): Promise; @@ -221,6 +224,16 @@ export function runWorkerQueueThreads( results: TResult[]; }>; +// @public +export class SuccessCache { + constructor(name: string, basePath?: string); + // (undocumented) + read(): Promise>; + static trimPaths(input: string): string; + // (undocumented) + write(newEntries: Iterable): Promise; +} + // @public export type WorkerQueueThreadsOptions = { items: Iterable; diff --git a/packages/cli/src/lib/cache/SuccessCache.ts b/packages/cli-node/src/cache/SuccessCache.ts similarity index 95% rename from packages/cli/src/lib/cache/SuccessCache.ts rename to packages/cli-node/src/cache/SuccessCache.ts index 8131a469a4..ba5c07ea30 100644 --- a/packages/cli/src/lib/cache/SuccessCache.ts +++ b/packages/cli-node/src/cache/SuccessCache.ts @@ -22,6 +22,12 @@ const DEFAULT_CACHE_BASE_PATH = 'node_modules/.cache/backstage-cli'; const CACHE_MAX_AGE_MS = 7 * 24 * 3600_000; +/** + * A file-system-based cache that tracks successful operations by storing + * timestamped marker files. + * + * @public + */ export class SuccessCache { readonly #path: string; @@ -89,7 +95,6 @@ export class SuccessCache { const empty = Buffer.alloc(0); for (const key of newEntries) { - // Remove any existing items with the key we're about to add const trimmedItems = existingItems.filter(item => item.endsWith(`_${key}`), ); diff --git a/packages/cli-node/src/cache/index.ts b/packages/cli-node/src/cache/index.ts new file mode 100644 index 0000000000..53c9f0e4e4 --- /dev/null +++ b/packages/cli-node/src/cache/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { SuccessCache } from './SuccessCache'; diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index 5540666a05..35074507c4 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -20,7 +20,9 @@ * @packageDocumentation */ +export * from './cache'; +export * from './concurrency'; export * from './git'; export * from './monorepo'; -export * from './concurrency'; export * from './roles'; +export * from './yarn'; diff --git a/packages/cli-node/src/yarn/index.ts b/packages/cli-node/src/yarn/index.ts new file mode 100644 index 0000000000..21b2efca3a --- /dev/null +++ b/packages/cli-node/src/yarn/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { getHasYarnPlugin } from './yarnPlugin'; diff --git a/packages/cli/src/lib/yarnPlugin.test.ts b/packages/cli-node/src/yarn/yarnPlugin.test.ts similarity index 100% rename from packages/cli/src/lib/yarnPlugin.test.ts rename to packages/cli-node/src/yarn/yarnPlugin.test.ts diff --git a/packages/cli/src/lib/yarnPlugin.ts b/packages/cli-node/src/yarn/yarnPlugin.ts similarity index 96% rename from packages/cli/src/lib/yarnPlugin.ts rename to packages/cli-node/src/yarn/yarnPlugin.ts index 0390345651..5be89704ea 100644 --- a/packages/cli/src/lib/yarnPlugin.ts +++ b/packages/cli-node/src/yarn/yarnPlugin.ts @@ -33,12 +33,12 @@ const yarnRcSchema = z.object({ * Detects whether the Backstage Yarn plugin is installed in the target repository. * * @returns Promise - true if the plugin is installed, false otherwise + * @public */ export async function getHasYarnPlugin(): Promise { 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 return ''; } throw e; diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index d0b4f6f37e..ad560935cd 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -28,7 +28,7 @@ import { import { targetPaths } from '@backstage/cli-common'; import { createScriptOptionsParser } from '../../lib/optionsParser'; -import { SuccessCache } from '../../../../lib/cache/SuccessCache'; +import { SuccessCache } from '@backstage/cli-node'; function depCount(pkg: BackstagePackageJson) { const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0; diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 7ecd908d8a..943bd3ab71 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -30,8 +30,11 @@ import { OptionValues } from 'commander'; import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'node:path'; -import { getHasYarnPlugin } from '../../../../lib/yarnPlugin'; -import { Lockfile, runConcurrentTasks } from '@backstage/cli-node'; +import { + getHasYarnPlugin, + Lockfile, + runConcurrentTasks, +} from '@backstage/cli-node'; import { fetchPackageInfo, mapDependencies, diff --git a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts index 45478957ac..bc8ff19307 100644 --- a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts +++ b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts @@ -28,7 +28,7 @@ import { Lockfile } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; import { createPackageVersionProvider } from '../../../../lib/version'; -import { getHasYarnPlugin } from '../../../../lib/yarnPlugin'; +import { getHasYarnPlugin } from '@backstage/cli-node'; const builtInHelpers = { camelCase, diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli/src/modules/test/commands/repo/test.ts index 1f57279935..2e4095f88a 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.ts @@ -31,7 +31,7 @@ import { findOwnPaths, isChildPath, } from '@backstage/cli-common'; -import { SuccessCache } from '../../../../lib/cache/SuccessCache'; +import { SuccessCache } from '@backstage/cli-node'; type JestProject = { displayName: string; diff --git a/yarn.lock b/yarn.lock index 74484a0056..1447977846 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3277,6 +3277,7 @@ __metadata: "@yarnpkg/parsers": "npm:^3.0.0" fs-extra: "npm:^11.2.0" semver: "npm:^7.5.3" + yaml: "npm:^2.0.0" zod: "npm:^3.25.76" languageName: unknown linkType: soft From 968570bbb54189349c106a84f1186eeba6bb80db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 25 Feb 2026 12:46:55 +0100 Subject: [PATCH 2/4] Address review feedback - Remove cli changeset, piggy-back on existing ones - Rename getHasYarnPlugin -> hasYarnPlugin(workspaceDir?) - Make SuccessCache constructor private, add static create() - Consolidate duplicate @backstage/cli-node imports Signed-off-by: Patrik Oldsberg --- .changeset/cli-use-cli-node-utils.md | 5 ---- .changeset/move-utils-to-cli-node.md | 2 +- packages/cli-node/report.api.md | 9 +++--- packages/cli-node/src/cache/SuccessCache.ts | 6 +++- packages/cli-node/src/yarn/index.ts | 2 +- packages/cli-node/src/yarn/yarnPlugin.test.ts | 29 ++++++++++--------- packages/cli-node/src/yarn/yarnPlugin.ts | 13 ++++++--- .../src/modules/lint/commands/repo/lint.ts | 4 +-- .../modules/migrate/commands/versions/bump.ts | 12 ++++---- .../new/lib/execution/PortableTemplater.ts | 6 ++-- .../src/modules/test/commands/repo/test.ts | 5 ++-- 11 files changed, 49 insertions(+), 44 deletions(-) delete mode 100644 .changeset/cli-use-cli-node-utils.md diff --git a/.changeset/cli-use-cli-node-utils.md b/.changeset/cli-use-cli-node-utils.md deleted file mode 100644 index 6b1d6be92d..0000000000 --- a/.changeset/cli-use-cli-node-utils.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Migrated `getHasYarnPlugin` and `SuccessCache` to use the implementations from `@backstage/cli-node`. diff --git a/.changeset/move-utils-to-cli-node.md b/.changeset/move-utils-to-cli-node.md index cf91e0b54f..7c14536ccf 100644 --- a/.changeset/move-utils-to-cli-node.md +++ b/.changeset/move-utils-to-cli-node.md @@ -2,4 +2,4 @@ '@backstage/cli-node': patch --- -Added `getHasYarnPlugin` and `SuccessCache` exports, moved from `@backstage/cli`. +Added `hasYarnPlugin` and `SuccessCache` exports, moved from `@backstage/cli`. diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 372bf34206..187f56640c 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -93,15 +93,15 @@ export type ConcurrentTasksOptions = { worker: (item: TItem) => Promise; }; -// @public -export function getHasYarnPlugin(): Promise; - // @public export class GitUtils { static listChangedFiles(ref: string): Promise; static readFileAtRef(path: string, ref: string): Promise; } +// @public +export function hasYarnPlugin(workspaceDir?: string): Promise; + // @public export function isMonoRepo(): Promise; @@ -226,7 +226,8 @@ export function runWorkerQueueThreads( // @public export class SuccessCache { - constructor(name: string, basePath?: string); + // (undocumented) + static create(name: string, basePath?: string): SuccessCache; // (undocumented) read(): Promise>; static trimPaths(input: string): string; diff --git a/packages/cli-node/src/cache/SuccessCache.ts b/packages/cli-node/src/cache/SuccessCache.ts index ba5c07ea30..62bef34137 100644 --- a/packages/cli-node/src/cache/SuccessCache.ts +++ b/packages/cli-node/src/cache/SuccessCache.ts @@ -40,7 +40,11 @@ export class SuccessCache { return input.replaceAll(targetPaths.rootDir, ''); } - constructor(name: string, basePath?: string) { + static create(name: string, basePath?: string): SuccessCache { + return new SuccessCache(name, basePath); + } + + private constructor(name: string, basePath?: string) { this.#path = resolvePath(basePath ?? DEFAULT_CACHE_BASE_PATH, name); } diff --git a/packages/cli-node/src/yarn/index.ts b/packages/cli-node/src/yarn/index.ts index 21b2efca3a..ce4eb4231c 100644 --- a/packages/cli-node/src/yarn/index.ts +++ b/packages/cli-node/src/yarn/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { getHasYarnPlugin } from './yarnPlugin'; +export { hasYarnPlugin } from './yarnPlugin'; diff --git a/packages/cli-node/src/yarn/yarnPlugin.test.ts b/packages/cli-node/src/yarn/yarnPlugin.test.ts index a07c2a2ac6..aba1dda579 100644 --- a/packages/cli-node/src/yarn/yarnPlugin.test.ts +++ b/packages/cli-node/src/yarn/yarnPlugin.test.ts @@ -16,12 +16,12 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; -import { getHasYarnPlugin } from './yarnPlugin'; +import { hasYarnPlugin } from './yarnPlugin'; const mockDir = createMockDirectory(); overrideTargetPaths(mockDir.path); -describe('getHasYarnPlugin', () => { +describe('hasYarnPlugin', () => { beforeEach(() => { mockDir.clear(); }); @@ -29,7 +29,7 @@ describe('getHasYarnPlugin', () => { it('should return false when .yarnrc.yml does not exist', async () => { mockDir.setContent({}); - const result = await getHasYarnPlugin(); + const result = await hasYarnPlugin(); expect(result).toBe(false); }); @@ -38,7 +38,7 @@ describe('getHasYarnPlugin', () => { '.yarnrc.yml': '', }); - const result = await getHasYarnPlugin(); + const result = await hasYarnPlugin(); expect(result).toBe(false); }); @@ -47,7 +47,7 @@ describe('getHasYarnPlugin', () => { '.yarnrc.yml': 'plugins: []', }); - const result = await getHasYarnPlugin(); + const result = await hasYarnPlugin(); expect(result).toBe(false); }); @@ -60,7 +60,7 @@ plugins: `, }); - const result = await getHasYarnPlugin(); + const result = await hasYarnPlugin(); expect(result).toBe(false); }); @@ -74,7 +74,7 @@ plugins: `, }); - const result = await getHasYarnPlugin(); + const result = await hasYarnPlugin(); expect(result).toBe(true); }); @@ -86,7 +86,7 @@ plugins: `, }); - const result = await getHasYarnPlugin(); + const result = await hasYarnPlugin(); expect(result).toBe(true); }); @@ -95,7 +95,7 @@ plugins: '.yarnrc.yml': 'invalid: yaml: content: [', }); - await expect(getHasYarnPlugin()).rejects.toThrow(); + await expect(hasYarnPlugin()).rejects.toThrow(); }); it('should throw error when .yarnrc.yml has unexpected structure', async () => { @@ -105,21 +105,22 @@ plugins: "not an array" `, }); - await expect(getHasYarnPlugin()).rejects.toThrow( + await expect(hasYarnPlugin()).rejects.toThrow( 'Unexpected content in .yarnrc.yml', ); }); - it('should handle plugins with different structure', async () => { + it('should resolve from a custom workspace directory', async () => { mockDir.setContent({ - '.yarnrc.yml': ` + 'custom-dir': { + '.yarnrc.yml': ` plugins: - path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs - - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs `, + }, }); - const result = await getHasYarnPlugin(); + const result = await hasYarnPlugin(mockDir.resolve('custom-dir')); expect(result).toBe(true); }); }); diff --git a/packages/cli-node/src/yarn/yarnPlugin.ts b/packages/cli-node/src/yarn/yarnPlugin.ts index 5be89704ea..87a3b98973 100644 --- a/packages/cli-node/src/yarn/yarnPlugin.ts +++ b/packages/cli-node/src/yarn/yarnPlugin.ts @@ -15,6 +15,7 @@ */ import fs from 'fs-extra'; +import { resolve as resolvePath } from 'node:path'; import yaml from 'yaml'; import z from 'zod'; import { targetPaths } from '@backstage/cli-common'; @@ -30,13 +31,17 @@ const yarnRcSchema = z.object({ }); /** - * Detects whether the Backstage Yarn plugin is installed in the target repository. + * Detects whether the Backstage Yarn plugin is installed in the given workspace directory. * - * @returns Promise - true if the plugin is installed, false otherwise + * @param workspaceDir - The workspace root directory to check. Defaults to the target root. + * @returns Promise resolving to true if the plugin is installed, false otherwise * @public */ -export async function getHasYarnPlugin(): Promise { - const yarnRcPath = targetPaths.resolveRoot('.yarnrc.yml'); +export async function hasYarnPlugin(workspaceDir?: string): Promise { + const yarnRcPath = resolvePath( + workspaceDir ?? targetPaths.rootDir, + '.yarnrc.yml', + ); const yarnRcContent = await fs.readFile(yarnRcPath, 'utf-8').catch(e => { if (e.code === 'ENOENT') { return ''; diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index ad560935cd..4351725d57 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -24,11 +24,11 @@ import { BackstagePackageJson, Lockfile, runWorkerQueueThreads, + SuccessCache, } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; import { createScriptOptionsParser } from '../../lib/optionsParser'; -import { SuccessCache } from '@backstage/cli-node'; function depCount(pkg: BackstagePackageJson) { const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0; @@ -41,7 +41,7 @@ function depCount(pkg: BackstagePackageJson) { export async function command(opts: OptionValues, cmd: Command): Promise { let packages = await PackageGraph.listTargetPackages(); - const cache = new SuccessCache('lint', opts.successCacheDir); + const cache = SuccessCache.create('lint', opts.successCacheDir); const cacheContext = opts.successCache ? { entries: await cache.read(), diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 943bd3ab71..4b15da4676 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -31,7 +31,7 @@ import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'node:path'; import { - getHasYarnPlugin, + hasYarnPlugin, Lockfile, runConcurrentTasks, } from '@backstage/cli-node'; @@ -76,7 +76,7 @@ function extendsDefaultPattern(pattern: string): boolean { export default async (opts: OptionValues) => { const lockfilePath = targetPaths.resolveRoot('yarn.lock'); const lockfile = await Lockfile.load(lockfilePath); - const hasYarnPlugin = await getHasYarnPlugin(); + const yarnPluginEnabled = await hasYarnPlugin(); let pattern = opts.pattern; @@ -130,7 +130,7 @@ export default async (opts: OptionValues) => { }); } - if (hasYarnPlugin) { + if (yarnPluginEnabled) { console.log(); console.log( `Updating yarn plugin to v${releaseManifest.releaseVersion}...`, @@ -214,7 +214,7 @@ export default async (opts: OptionValues) => { const oldLockfileRange = await asLockfileVersion(oldRange); const useBackstageRange = - hasYarnPlugin && + yarnPluginEnabled && // Only use backstage:^ versions if the package is present in // the manifest for the release we're bumping to. releaseManifest.packages.find( @@ -254,7 +254,7 @@ export default async (opts: OptionValues) => { if (extendsDefaultPattern(pattern)) { await bumpBackstageJsonVersion( releaseManifest.releaseVersion, - hasYarnPlugin, + yarnPluginEnabled, ); } else { console.log( @@ -319,7 +319,7 @@ export default async (opts: OptionValues) => { console.log(); } - if (hasYarnPlugin) { + if (yarnPluginEnabled) { console.log(); console.log( chalk.blue( diff --git a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts index bc8ff19307..2c94e3639d 100644 --- a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts +++ b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts @@ -28,7 +28,7 @@ import { Lockfile } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; import { createPackageVersionProvider } from '../../../../lib/version'; -import { getHasYarnPlugin } from '@backstage/cli-node'; +import { hasYarnPlugin } from '@backstage/cli-node'; const builtInHelpers = { camelCase, @@ -55,9 +55,9 @@ export class PortableTemplater { /* ignored */ } - const hasYarnPlugin = await getHasYarnPlugin(); + const yarnPluginEnabled = await hasYarnPlugin(); const versionProvider = createPackageVersionProvider(lockfile, { - preferBackstageProtocol: hasYarnPlugin, + preferBackstageProtocol: yarnPluginEnabled, }); const templater = new PortableTemplater( diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli/src/modules/test/commands/repo/test.ts index 2e4095f88a..0ccac5c9b5 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.ts @@ -22,7 +22,7 @@ import yargs from 'yargs'; import { run as runJest, yargsOptions as jestYargsOptions } from 'jest-cli'; import { relative as relativePath } from 'node:path'; import { Command, OptionValues } from 'commander'; -import { Lockfile, PackageGraph } from '@backstage/cli-node'; +import { Lockfile, PackageGraph, SuccessCache } from '@backstage/cli-node'; import { runCheck, @@ -31,7 +31,6 @@ import { findOwnPaths, isChildPath, } from '@backstage/cli-common'; -import { SuccessCache } from '@backstage/cli-node'; type JestProject = { displayName: string; @@ -333,7 +332,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { ); } - const cache = new SuccessCache('test', opts.successCacheDir); + const cache = SuccessCache.create('test', opts.successCacheDir); const graph = await getPackageGraph(); // Shared state for the bridge From 09eb6b518779f8328969f90dae90a534e3dd2c68 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 25 Feb 2026 15:25:11 +0100 Subject: [PATCH 3/4] Address further review feedback - Rename hasYarnPlugin -> hasBackstageYarnPlugin for clarity - Change SuccessCache.create to accept an options object Signed-off-by: Patrik Oldsberg --- .changeset/move-utils-to-cli-node.md | 2 +- packages/cli-node/report.api.md | 4 ++-- packages/cli-node/src/cache/SuccessCache.ts | 11 ++++++---- packages/cli-node/src/yarn/index.ts | 2 +- packages/cli-node/src/yarn/yarnPlugin.test.ts | 22 +++++++++---------- packages/cli-node/src/yarn/yarnPlugin.ts | 4 +++- .../src/modules/lint/commands/repo/lint.ts | 5 ++++- .../modules/migrate/commands/versions/bump.ts | 4 ++-- .../new/lib/execution/PortableTemplater.ts | 4 ++-- .../src/modules/test/commands/repo/test.ts | 5 ++++- 10 files changed, 37 insertions(+), 26 deletions(-) diff --git a/.changeset/move-utils-to-cli-node.md b/.changeset/move-utils-to-cli-node.md index 7c14536ccf..f708dde666 100644 --- a/.changeset/move-utils-to-cli-node.md +++ b/.changeset/move-utils-to-cli-node.md @@ -2,4 +2,4 @@ '@backstage/cli-node': patch --- -Added `hasYarnPlugin` and `SuccessCache` exports, moved from `@backstage/cli`. +Added `hasBackstageYarnPlugin` and `SuccessCache` exports, moved from `@backstage/cli`. diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 187f56640c..9d4887ca59 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -100,7 +100,7 @@ export class GitUtils { } // @public -export function hasYarnPlugin(workspaceDir?: string): Promise; +export function hasBackstageYarnPlugin(workspaceDir?: string): Promise; // @public export function isMonoRepo(): Promise; @@ -227,7 +227,7 @@ export function runWorkerQueueThreads( // @public export class SuccessCache { // (undocumented) - static create(name: string, basePath?: string): SuccessCache; + static create(options: { name: string; basePath?: string }): SuccessCache; // (undocumented) read(): Promise>; static trimPaths(input: string): string; diff --git a/packages/cli-node/src/cache/SuccessCache.ts b/packages/cli-node/src/cache/SuccessCache.ts index 62bef34137..78913ed0b6 100644 --- a/packages/cli-node/src/cache/SuccessCache.ts +++ b/packages/cli-node/src/cache/SuccessCache.ts @@ -40,12 +40,15 @@ export class SuccessCache { return input.replaceAll(targetPaths.rootDir, ''); } - static create(name: string, basePath?: string): SuccessCache { - return new SuccessCache(name, basePath); + static create(options: { name: string; basePath?: string }): SuccessCache { + return new SuccessCache(options); } - private constructor(name: string, basePath?: string) { - this.#path = resolvePath(basePath ?? DEFAULT_CACHE_BASE_PATH, name); + private constructor(options: { name: string; basePath?: string }) { + this.#path = resolvePath( + options.basePath ?? DEFAULT_CACHE_BASE_PATH, + options.name, + ); } async read(): Promise> { diff --git a/packages/cli-node/src/yarn/index.ts b/packages/cli-node/src/yarn/index.ts index ce4eb4231c..871aa14ca8 100644 --- a/packages/cli-node/src/yarn/index.ts +++ b/packages/cli-node/src/yarn/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { hasYarnPlugin } from './yarnPlugin'; +export { hasBackstageYarnPlugin } from './yarnPlugin'; diff --git a/packages/cli-node/src/yarn/yarnPlugin.test.ts b/packages/cli-node/src/yarn/yarnPlugin.test.ts index aba1dda579..ecbff45ba7 100644 --- a/packages/cli-node/src/yarn/yarnPlugin.test.ts +++ b/packages/cli-node/src/yarn/yarnPlugin.test.ts @@ -16,12 +16,12 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; -import { hasYarnPlugin } from './yarnPlugin'; +import { hasBackstageYarnPlugin } from './yarnPlugin'; const mockDir = createMockDirectory(); overrideTargetPaths(mockDir.path); -describe('hasYarnPlugin', () => { +describe('hasBackstageYarnPlugin', () => { beforeEach(() => { mockDir.clear(); }); @@ -29,7 +29,7 @@ describe('hasYarnPlugin', () => { it('should return false when .yarnrc.yml does not exist', async () => { mockDir.setContent({}); - const result = await hasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(false); }); @@ -38,7 +38,7 @@ describe('hasYarnPlugin', () => { '.yarnrc.yml': '', }); - const result = await hasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(false); }); @@ -47,7 +47,7 @@ describe('hasYarnPlugin', () => { '.yarnrc.yml': 'plugins: []', }); - const result = await hasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(false); }); @@ -60,7 +60,7 @@ plugins: `, }); - const result = await hasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(false); }); @@ -74,7 +74,7 @@ plugins: `, }); - const result = await hasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(true); }); @@ -86,7 +86,7 @@ plugins: `, }); - const result = await hasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(true); }); @@ -95,7 +95,7 @@ plugins: '.yarnrc.yml': 'invalid: yaml: content: [', }); - await expect(hasYarnPlugin()).rejects.toThrow(); + await expect(hasBackstageYarnPlugin()).rejects.toThrow(); }); it('should throw error when .yarnrc.yml has unexpected structure', async () => { @@ -105,7 +105,7 @@ plugins: "not an array" `, }); - await expect(hasYarnPlugin()).rejects.toThrow( + await expect(hasBackstageYarnPlugin()).rejects.toThrow( 'Unexpected content in .yarnrc.yml', ); }); @@ -120,7 +120,7 @@ plugins: }, }); - const result = await hasYarnPlugin(mockDir.resolve('custom-dir')); + const result = await hasBackstageYarnPlugin(mockDir.resolve('custom-dir')); expect(result).toBe(true); }); }); diff --git a/packages/cli-node/src/yarn/yarnPlugin.ts b/packages/cli-node/src/yarn/yarnPlugin.ts index 87a3b98973..65383edd0c 100644 --- a/packages/cli-node/src/yarn/yarnPlugin.ts +++ b/packages/cli-node/src/yarn/yarnPlugin.ts @@ -37,7 +37,9 @@ const yarnRcSchema = z.object({ * @returns Promise resolving to true if the plugin is installed, false otherwise * @public */ -export async function hasYarnPlugin(workspaceDir?: string): Promise { +export async function hasBackstageYarnPlugin( + workspaceDir?: string, +): Promise { const yarnRcPath = resolvePath( workspaceDir ?? targetPaths.rootDir, '.yarnrc.yml', diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index 4351725d57..a3ae69302c 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -41,7 +41,10 @@ function depCount(pkg: BackstagePackageJson) { export async function command(opts: OptionValues, cmd: Command): Promise { let packages = await PackageGraph.listTargetPackages(); - const cache = SuccessCache.create('lint', opts.successCacheDir); + const cache = SuccessCache.create({ + name: 'lint', + basePath: opts.successCacheDir, + }); const cacheContext = opts.successCache ? { entries: await cache.read(), diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 4b15da4676..8fe7013ac8 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -31,7 +31,7 @@ import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'node:path'; import { - hasYarnPlugin, + hasBackstageYarnPlugin, Lockfile, runConcurrentTasks, } from '@backstage/cli-node'; @@ -76,7 +76,7 @@ function extendsDefaultPattern(pattern: string): boolean { export default async (opts: OptionValues) => { const lockfilePath = targetPaths.resolveRoot('yarn.lock'); const lockfile = await Lockfile.load(lockfilePath); - const yarnPluginEnabled = await hasYarnPlugin(); + const yarnPluginEnabled = await hasBackstageYarnPlugin(); let pattern = opts.pattern; diff --git a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts index 2c94e3639d..42285259b3 100644 --- a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts +++ b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts @@ -28,7 +28,7 @@ import { Lockfile } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; import { createPackageVersionProvider } from '../../../../lib/version'; -import { hasYarnPlugin } from '@backstage/cli-node'; +import { hasBackstageYarnPlugin } from '@backstage/cli-node'; const builtInHelpers = { camelCase, @@ -55,7 +55,7 @@ export class PortableTemplater { /* ignored */ } - const yarnPluginEnabled = await hasYarnPlugin(); + const yarnPluginEnabled = await hasBackstageYarnPlugin(); const versionProvider = createPackageVersionProvider(lockfile, { preferBackstageProtocol: yarnPluginEnabled, }); diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli/src/modules/test/commands/repo/test.ts index 0ccac5c9b5..53f86fd635 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.ts @@ -332,7 +332,10 @@ export async function command(opts: OptionValues, cmd: Command): Promise { ); } - const cache = SuccessCache.create('test', opts.successCacheDir); + const cache = SuccessCache.create({ + name: 'test', + basePath: opts.successCacheDir, + }); const graph = await getPackageGraph(); // Shared state for the bridge From fb14783d9b137c51e7020a7b3ca03e7b3a726cf6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 25 Feb 2026 15:42:07 +0100 Subject: [PATCH 4/4] ci: trigger Signed-off-by: Patrik Oldsberg Co-authored-by: Cursor