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