diff --git a/.changeset/slick-dogs-pay.md b/.changeset/slick-dogs-pay.md new file mode 100644 index 0000000000..f75382aa02 --- /dev/null +++ b/.changeset/slick-dogs-pay.md @@ -0,0 +1,8 @@ +--- +'@backstage/cli-common': patch +'@backstage/create-app': patch +'@backstage/cli-node': patch +'@backstage/cli': patch +--- + +Properly support `package.json` `workspaces` field diff --git a/packages/cli-common/report.api.md b/packages/cli-common/report.api.md index 96475c6bd1..ec146f78c4 100644 --- a/packages/cli-common/report.api.md +++ b/packages/cli-common/report.api.md @@ -23,6 +23,9 @@ export class ExitCodeError extends CustomErrorBase { // @public export function findPaths(searchDir: string): Paths; +// @public +export function getWorkspacesPatterns(pkgJson: any): 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 d11601b044..146af314cd 100644 --- a/packages/cli-common/src/index.ts +++ b/packages/cli-common/src/index.ts @@ -20,7 +20,7 @@ * @packageDocumentation */ -export { findPaths, BACKSTAGE_JSON } from './paths'; +export { findPaths, getWorkspacesPatterns, BACKSTAGE_JSON } from './paths'; 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 5da2d596a4..557af7ccca 100644 --- a/packages/cli-common/src/paths.ts +++ b/packages/cli-common/src/paths.ts @@ -107,6 +107,25 @@ export function findOwnRootDir(ownDir: string) { return resolvePath(ownDir, '../..'); } +/** + * Gets the workspaces pattern from a package.json object. + * @param pkgJson - The package.json object to get the workspaces pattern from. + * @returns The workspaces patterns (glob not resolved). + * @public + */ +export function getWorkspacesPatterns(pkgJson: any): string[] { + if (Array.isArray(pkgJson.workspaces)) { + return pkgJson.workspaces; + } else if ( + typeof pkgJson.workspaces === 'object' && + pkgJson.workspaces !== null && + Array.isArray(pkgJson.workspaces.packages) + ) { + return pkgJson.workspaces.packages; + } + return []; +} + /** * Find paths related to a package and its execution context. * @@ -141,7 +160,7 @@ export function findPaths(searchDir: string): Paths { try { const content = fs.readFileSync(path, 'utf8'); const data = JSON.parse(content); - return Boolean(data.workspaces); + return getWorkspacesPatterns(data).length > 0; } catch (error) { throw new Error( `Failed to parse package.json file while searching for root, ${error}`, diff --git a/packages/cli-node/src/monorepo/isMonoRepo.ts b/packages/cli-node/src/monorepo/isMonoRepo.ts index da78c8dd53..9cb19ea62f 100644 --- a/packages/cli-node/src/monorepo/isMonoRepo.ts +++ b/packages/cli-node/src/monorepo/isMonoRepo.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { getWorkspacesPatterns } from '@backstage/cli-common'; import { paths } from '../paths'; import fs from 'fs-extra'; @@ -26,7 +27,7 @@ export async function isMonoRepo(): Promise { const rootPackageJsonPath = paths.resolveTargetRoot('package.json'); try { const pkg = await fs.readJson(rootPackageJsonPath); - return Boolean(pkg?.workspaces?.packages); + return getWorkspacesPatterns(pkg).length > 0; } 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 dd35e74902..fd7ace5bcb 100644 --- a/packages/cli-node/src/pacman/PackageManager.test.ts +++ b/packages/cli-node/src/pacman/PackageManager.test.ts @@ -43,7 +43,7 @@ describe('PackageManager', () => { expect(mockYarnCreate).toHaveBeenCalled(); }); - it('should detect via root package.json workspaces', async () => { + it('should detect via root package.json workspaces with legacy workspaces.packages field', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ name: 'foo', @@ -55,6 +55,16 @@ describe('PackageManager', () => { await detectPackageManager(); expect(mockYarnCreate).toHaveBeenCalled(); }); + it('should detect via root package.json workspaces', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + name: 'foo', + workspaces: [], + }), + }); + await detectPackageManager(); + expect(mockYarnCreate).toHaveBeenCalled(); + }); it('should detect via root package.json packageManager', async () => { mockDir.setContent({ diff --git a/packages/cli-node/src/pacman/PackageManager.ts b/packages/cli-node/src/pacman/PackageManager.ts index 44c0849705..5693acce06 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 { RunOptions } from '@backstage/cli-common'; +import { getWorkspacesPatterns, 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 (packageJson.workspaces) { + if (getWorkspacesPatterns(packageJson).length > 0) { // technically this could be NPM as well return await Yarn.create(); } diff --git a/packages/cli-node/src/pacman/yarn/Yarn.test.ts b/packages/cli-node/src/pacman/yarn/Yarn.test.ts index 9bf866b821..f13b5a0296 100644 --- a/packages/cli-node/src/pacman/yarn/Yarn.test.ts +++ b/packages/cli-node/src/pacman/yarn/Yarn.test.ts @@ -30,7 +30,7 @@ const allYarnVersions = [yarnClassic, yarnBerry]; describe('Yarn', () => { describe.each(allYarnVersions)('%s.getMonorepoPackages', yarn => { - it('should detect a monorepo', async () => { + it('should detect a monorepo with workspaces.packages field', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ name: 'foo', @@ -41,6 +41,15 @@ describe('Yarn', () => { }); await expect(yarn.getMonorepoPackages()).resolves.toEqual(['packages/*']); }); + it('should detect a monorepo', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + name: 'foo', + workspaces: ['packages/*'], + }), + }); + await expect(yarn.getMonorepoPackages()).resolves.toEqual(['packages/*']); + }); it('should detect a non-monorepo', async () => { mockDir.setContent({ diff --git a/packages/cli-node/src/pacman/yarn/Yarn.ts b/packages/cli-node/src/pacman/yarn/Yarn.ts index a6544344ca..4658a256c6 100644 --- a/packages/cli-node/src/pacman/yarn/Yarn.ts +++ b/packages/cli-node/src/pacman/yarn/Yarn.ts @@ -25,6 +25,7 @@ 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) {} @@ -50,7 +51,7 @@ export class Yarn implements PackageManager { const rootPackageJsonPath = paths.resolveTargetRoot('package.json'); try { const pkg = await fs.readJson(rootPackageJsonPath); - return pkg?.workspaces?.packages || []; + return getWorkspacesPatterns(pkg); } catch (error) { return []; } diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 814f017e1d..09143ac034 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -20,6 +20,7 @@ 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, @@ -336,11 +337,10 @@ async function getRootConfig() { rejectFrontendNetworkRequests, }; - const workspacePatterns = - rootPkgJson.workspaces && rootPkgJson.workspaces.packages; + const workspacePatterns = getWorkspacesPatterns(rootPkgJson); // Check if we're running within a specific monorepo package. In that case just get the single project config. - if (!workspacePatterns || paths.targetRoot !== paths.targetDir) { + if (workspacePatterns.length === 0 || paths.targetRoot !== paths.targetDir) { return getProjectConfig( paths.targetDir, { diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli/src/lib/versioning/packages.test.ts index e823ce8769..8946b8ba2f 100644 --- a/packages/cli/src/lib/versioning/packages.test.ts +++ b/packages/cli/src/lib/versioning/packages.test.ts @@ -104,9 +104,7 @@ describe('mapDependencies', () => { it('should read dependencies', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ - workspaces: { - packages: ['pkgs/*'], - }, + workspaces: ['pkgs/*'], }), pkgs: { a: { diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts index 202704e030..affd5a9aee 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts @@ -160,9 +160,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -255,9 +253,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -353,9 +349,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -459,9 +453,7 @@ describe('bump', () => { '.yarnrc.yml': yarnRcMock, 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -572,9 +564,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -644,9 +634,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -750,9 +738,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': customLockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -865,9 +851,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1099,9 +1083,7 @@ describe('environment variables', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1192,9 +1174,7 @@ describe('environment variables', () => { 'custom-manifest.json': JSON.stringify(customManifest), 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1262,9 +1242,7 @@ describe('environment variables', () => { '.yarnrc.yml': yarnRcMock, 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1337,9 +1315,7 @@ describe('environment variables', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1362,9 +1338,7 @@ describe('environment variables', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { diff --git a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts b/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts index 936d10e24c..533bdaa954 100644 --- a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts @@ -73,9 +73,7 @@ describe('versions:migrate', () => { it('should bump to the moved version when the package is moved', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), node_modules: { '@backstage': { @@ -177,9 +175,7 @@ describe('versions:migrate', () => { it('should replace the occurrences of the moved package in files inside the correct package', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), node_modules: { '@backstage': { @@ -264,9 +260,7 @@ describe('versions:migrate', () => { it('should replace occurrences of changed packages, and is careful', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), node_modules: { '@backstage': {