diff --git a/.changeset/slick-dogs-pay.md b/.changeset/slick-dogs-pay.md index f75382aa02..409670601e 100644 --- a/.changeset/slick-dogs-pay.md +++ b/.changeset/slick-dogs-pay.md @@ -1,5 +1,4 @@ --- -'@backstage/cli-common': patch '@backstage/create-app': patch '@backstage/cli-node': patch '@backstage/cli': patch diff --git a/packages/cli-common/report.api.md b/packages/cli-common/report.api.md index 324b28880b..96475c6bd1 100644 --- a/packages/cli-common/report.api.md +++ b/packages/cli-common/report.api.md @@ -23,9 +23,6 @@ export class ExitCodeError extends CustomErrorBase { // @public export function findPaths(searchDir: string): Paths; -// @public -export function getWorkspacesPatterns(pkgJson: unknown): string[]; - // @public export function isChildPath(base: string, path: string): boolean; diff --git a/packages/cli-common/src/index.ts b/packages/cli-common/src/index.ts index e142030b88..d11601b044 100644 --- a/packages/cli-common/src/index.ts +++ b/packages/cli-common/src/index.ts @@ -21,7 +21,6 @@ */ export { findPaths, BACKSTAGE_JSON } from './paths'; -export { getWorkspacesPatterns } from './workspaces'; export { isChildPath } from './isChildPath'; export type { Paths, ResolveFunc } from './paths'; export { bootstrapEnvProxyAgents } from './proxyBootstrap'; diff --git a/packages/cli-common/src/paths.ts b/packages/cli-common/src/paths.ts index efbb05c7bb..5da2d596a4 100644 --- a/packages/cli-common/src/paths.ts +++ b/packages/cli-common/src/paths.ts @@ -16,7 +16,6 @@ import fs from 'node:fs'; import { dirname, resolve as resolvePath } from 'node:path'; -import { getWorkspacesPatterns } from './workspaces'; /** * A function that takes a set of path fragments and resolves them into a @@ -142,7 +141,7 @@ export function findPaths(searchDir: string): Paths { try { const content = fs.readFileSync(path, 'utf8'); const data = JSON.parse(content); - return getWorkspacesPatterns(data).length > 0; + return Boolean(data.workspaces); } catch (error) { throw new Error( `Failed to parse package.json file while searching for root, ${error}`, diff --git a/packages/cli-common/src/workspaces.test.ts b/packages/cli-common/src/workspaces.test.ts deleted file mode 100644 index 7725aa7e6a..0000000000 --- a/packages/cli-common/src/workspaces.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2026 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getWorkspacesPatterns } from './workspaces'; - -describe('getWorkspacesPatterns', () => { - it('returns empty array when pkgJson has no workspaces key', () => { - expect(getWorkspacesPatterns({ name: 'pkg' })).toEqual([]); - }); - - it('returns patterns when workspaces is array form', () => { - const patterns = ['packages/*', 'apps/*']; - expect(getWorkspacesPatterns({ workspaces: patterns })).toEqual(patterns); - }); - - it('returns patterns when workspaces is object form with packages', () => { - const patterns = ['packages/*']; - expect( - getWorkspacesPatterns({ workspaces: { packages: patterns } }), - ).toEqual(patterns); - }); - - it('returns empty array when workspaces is object without packages array', () => { - expect(getWorkspacesPatterns({ workspaces: {} })).toEqual([]); - expect(getWorkspacesPatterns({ workspaces: { noMatch: [] } })).toEqual([]); - }); - - it('throws when pkgJson is not an object', () => { - expect(() => getWorkspacesPatterns(null)).toThrow( - 'pkgJson must be an object', - ); - expect(() => getWorkspacesPatterns('string')).toThrow( - 'pkgJson must be an object', - ); - expect(() => getWorkspacesPatterns(42)).toThrow( - 'pkgJson must be an object', - ); - }); - - it('throws when workspaces array contains non-strings', () => { - expect(() => getWorkspacesPatterns({ workspaces: ['a', 1, 'b'] })).toThrow( - 'must be an array of strings', - ); - expect(() => getWorkspacesPatterns({ workspaces: [null] })).toThrow( - 'must be an array of strings', - ); - }); - - it('throws when workspaces.packages contains non-strings', () => { - expect(() => - getWorkspacesPatterns({ - workspaces: { packages: ['a', 1] }, - }), - ).toThrow('must be an array of strings'); - }); -}); diff --git a/packages/cli-common/src/workspaces.ts b/packages/cli-common/src/workspaces.ts deleted file mode 100644 index 17fca511a4..0000000000 --- a/packages/cli-common/src/workspaces.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2026 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -function assertStringArray( - value: unknown, - field: string, -): asserts value is string[] { - if (!Array.isArray(value)) { - throw new Error(`${field} must be an array`); - } - if (!value.every((item): item is string => typeof item === 'string')) { - throw new Error(`${field} must be an array of strings`); - } -} - -/** - * Gets the workspaces patterns from a package.json object. - * - * Supports both the array form (`"workspaces": ["packages/*"]`) and the object - * form (`"workspaces": { "packages": ["packages/*"] }`) from npm/yarn. - * - * @param pkgJson - The package.json object. Type as `unknown` so callers must pass parsed JSON; an object type check is performed. - * @returns The workspaces patterns (globs not resolved). Empty array if none. - * @throws Error if `pkgJson` is not an object or workspaces/packages contain non-strings. - * @public - */ -export function getWorkspacesPatterns(pkgJson: unknown): string[] { - if (typeof pkgJson !== 'object' || pkgJson === null) { - throw new Error('pkgJson must be an object'); - } - if (!('workspaces' in pkgJson)) { - return []; - } - - const workspaces = pkgJson.workspaces; - if (Array.isArray(workspaces)) { - assertStringArray(workspaces, 'pkgJson.workspaces'); - return workspaces; - } - if ( - typeof workspaces === 'object' && - workspaces !== null && - 'packages' in workspaces && - Array.isArray(workspaces.packages) - ) { - assertStringArray(workspaces.packages, 'pkgJson.workspaces.packages'); - return workspaces.packages; - } - return []; -} diff --git a/packages/cli-node/src/monorepo/isMonoRepo.ts b/packages/cli-node/src/monorepo/isMonoRepo.ts index 9cb19ea62f..c58b3d00ce 100644 --- a/packages/cli-node/src/monorepo/isMonoRepo.ts +++ b/packages/cli-node/src/monorepo/isMonoRepo.ts @@ -14,12 +14,14 @@ * limitations under the License. */ -import { getWorkspacesPatterns } from '@backstage/cli-common'; import { paths } from '../paths'; import fs from 'fs-extra'; /** - * Returns try if the current project is a monorepo. + * Returns true if the current project is a monorepo. + * + * Uses a simple presence check on the `workspaces` field. Empty or invalid + * workspace config is treated as a monorepo; we do not validate patterns. * * @public */ @@ -27,7 +29,7 @@ export async function isMonoRepo(): Promise { const rootPackageJsonPath = paths.resolveTargetRoot('package.json'); try { const pkg = await fs.readJson(rootPackageJsonPath); - return getWorkspacesPatterns(pkg).length > 0; + return Boolean(pkg?.workspaces); } catch (error) { return false; } diff --git a/packages/cli-node/src/pacman/PackageManager.test.ts b/packages/cli-node/src/pacman/PackageManager.test.ts index e394d2ed60..f704610ab2 100644 --- a/packages/cli-node/src/pacman/PackageManager.test.ts +++ b/packages/cli-node/src/pacman/PackageManager.test.ts @@ -43,19 +43,19 @@ describe('PackageManager', () => { expect(mockYarnCreate).toHaveBeenCalled(); }); - it('should detect via root package.json workspaces with legacy workspaces.packages field', async () => { + it('should detect via root package.json workspaces', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ name: 'foo', workspaces: { - packages: ['packages/*'], + packages: [], }, }), }); await detectPackageManager(); expect(mockYarnCreate).toHaveBeenCalled(); }); - it('should detect via root package.json workspaces', async () => { + it('should detect via root package.json workspaces (array form)', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ name: 'foo', diff --git a/packages/cli-node/src/pacman/PackageManager.ts b/packages/cli-node/src/pacman/PackageManager.ts index 5693acce06..44c0849705 100644 --- a/packages/cli-node/src/pacman/PackageManager.ts +++ b/packages/cli-node/src/pacman/PackageManager.ts @@ -17,7 +17,7 @@ import { Yarn } from './yarn'; import { Lockfile } from './Lockfile'; import { paths } from '../paths'; -import { getWorkspacesPatterns, RunOptions } from '@backstage/cli-common'; +import { RunOptions } from '@backstage/cli-common'; import fs from 'fs-extra'; /** @@ -101,7 +101,7 @@ export async function detectPackageManager(): Promise { const packageJson = await fs.readJson( paths.resolveTargetRoot('package.json'), ); - if (getWorkspacesPatterns(packageJson).length > 0) { + if (packageJson.workspaces) { // technically this could be NPM as well return await Yarn.create(); } diff --git a/packages/cli-node/src/pacman/yarn/Yarn.ts b/packages/cli-node/src/pacman/yarn/Yarn.ts index 4658a256c6..9c242c90da 100644 --- a/packages/cli-node/src/pacman/yarn/Yarn.ts +++ b/packages/cli-node/src/pacman/yarn/Yarn.ts @@ -25,7 +25,6 @@ import { YarnVersion } from './types'; import fs from 'fs-extra'; import { paths } from '../../paths'; import { run, runOutput, RunOptions } from '@backstage/cli-common'; -import { getWorkspacesPatterns } from '@backstage/cli-common'; export class Yarn implements PackageManager { constructor(private readonly yarnVersion: YarnVersion) {} @@ -51,7 +50,10 @@ export class Yarn implements PackageManager { const rootPackageJsonPath = paths.resolveTargetRoot('package.json'); try { const pkg = await fs.readJson(rootPackageJsonPath); - return getWorkspacesPatterns(pkg); + const workspaces = pkg?.workspaces; + return Array.isArray(workspaces) + ? workspaces + : workspaces?.packages ?? []; } catch (error) { return []; } diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 09143ac034..c670f0fe8b 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -20,7 +20,6 @@ const crypto = require('node:crypto'); const glob = require('node:util').promisify(require('glob')); const { version } = require('../package.json'); const paths = require('@backstage/cli-common').findPaths(process.cwd()); -const { getWorkspacesPatterns } = require('@backstage/cli-common'); const { getJestEnvironment, getJestMajorVersion, @@ -337,10 +336,11 @@ async function getRootConfig() { rejectFrontendNetworkRequests, }; - const workspacePatterns = getWorkspacesPatterns(rootPkgJson); + const ws = rootPkgJson.workspaces; + const workspacePatterns = Array.isArray(ws) ? ws : ws?.packages; // Check if we're running within a specific monorepo package. In that case just get the single project config. - if (workspacePatterns.length === 0 || paths.targetRoot !== paths.targetDir) { + if (!workspacePatterns || paths.targetRoot !== paths.targetDir) { return getProjectConfig( paths.targetDir, {