From ebd463070223fe497aa821fd7bdb51466e49c783 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Thu, 29 Jan 2026 09:03:19 +0100 Subject: [PATCH 1/6] chore: replace legacy package.json workspaces.packages by workspaces Signed-off-by: Gabriel Dugny --- .changeset/slow-items-pull.md | 18 ++++++++++++++++++ docs/contribute/project-structure.md | 10 ++++------ package.json | 10 ++++------ .../templates/default-app/package.json.hbs | 10 ++++------ .../templates/next-app/package.json.hbs | 10 ++++------ 5 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 .changeset/slow-items-pull.md diff --git a/.changeset/slow-items-pull.md b/.changeset/slow-items-pull.md new file mode 100644 index 0000000000..50f4897fa5 --- /dev/null +++ b/.changeset/slow-items-pull.md @@ -0,0 +1,18 @@ +--- +'@backstage/create-app': patch +--- + +Replace `package.json` deprecated `workspaces.packages` by `workspaces` + +This change is **not** required, but you can edit your main `package.json`, to fit the more modern & more common pattern: + +```diff +- "workspaces": { +- "packages": [ + "workspaces": [ + "packages/*", + "plugins/*" +- ] +- }, + ], +``` diff --git a/docs/contribute/project-structure.md b/docs/contribute/project-structure.md index f58afedee8..372fb79844 100644 --- a/docs/contribute/project-structure.md +++ b/docs/contribute/project-structure.md @@ -55,12 +55,10 @@ defined in [`package.json`](https://github.com/backstage/backstage/blob/master/package.json): ```json - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, + "workspaces": [ + "packages/*", + "plugins/*" + ], ``` Let's look at them individually. diff --git a/package.json b/package.json index a0a530adb8..5a63fbc2db 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,10 @@ "type": "git", "url": "https://github.com/backstage/backstage" }, - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, + "workspaces": [ + "packages/*", + "plugins/*" + ], "scripts": { "build-storybook": "storybook build --output-dir dist-storybook", "build-storybook:chromatic": "STORYBOOK_STORY_SET=chromatic storybook build --stats-json --output-dir dist-storybook", diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 4e723ea078..229ed5ffa0 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -22,12 +22,10 @@ "prettier:check": "prettier --check .", "new": "backstage-cli new" }, - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, + "workspaces": [ + "packages/*", + "plugins/*" + ], "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", "@backstage/e2e-test-utils": "^{{version '@backstage/e2e-test-utils'}}", diff --git a/packages/create-app/templates/next-app/package.json.hbs b/packages/create-app/templates/next-app/package.json.hbs index 80ff4a9a06..27d5880c11 100644 --- a/packages/create-app/templates/next-app/package.json.hbs +++ b/packages/create-app/templates/next-app/package.json.hbs @@ -44,12 +44,10 @@ } } }, - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, + "workspaces": [ + "packages/*", + "plugins/*" + ], "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", "@backstage/e2e-test-utils": "^{{version '@backstage/e2e-test-utils'}}", From a9d23c4a322717b14648dfbc3c38d206a0f1cfd4 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Thu, 29 Jan 2026 10:24:31 +0100 Subject: [PATCH 2/6] fix: Support workspaces in CLIs Signed-off-by: Gabriel Dugny --- .changeset/slick-dogs-pay.md | 8 +++ packages/cli-common/report.api.md | 3 ++ packages/cli-common/src/index.ts | 2 +- packages/cli-common/src/paths.ts | 21 +++++++- packages/cli-node/src/monorepo/isMonoRepo.ts | 3 +- .../src/pacman/PackageManager.test.ts | 12 ++++- .../cli-node/src/pacman/PackageManager.ts | 4 +- .../cli-node/src/pacman/yarn/Yarn.test.ts | 11 +++- packages/cli-node/src/pacman/yarn/Yarn.ts | 3 +- packages/cli/config/jest.js | 6 +-- .../cli/src/lib/versioning/packages.test.ts | 4 +- .../migrate/commands/versions/bump.test.ts | 52 +++++-------------- .../migrate/commands/versions/migrate.test.ts | 12 ++--- 13 files changed, 79 insertions(+), 62 deletions(-) create mode 100644 .changeset/slick-dogs-pay.md 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': { From b2d9d78c27e1db07773945315819d9518684d437 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Thu, 29 Jan 2026 17:43:29 +0100 Subject: [PATCH 3/6] fix: defensive package.json reading Signed-off-by: Gabriel Dugny --- packages/cli-common/report.api.md | 2 +- packages/cli-common/src/index.ts | 3 +- packages/cli-common/src/paths.test.ts | 6 +- packages/cli-common/src/paths.ts | 20 +----- packages/cli-common/src/workspaces.test.ts | 69 +++++++++++++++++++ packages/cli-common/src/workspaces.ts | 63 +++++++++++++++++ .../src/pacman/PackageManager.test.ts | 4 +- 7 files changed, 142 insertions(+), 25 deletions(-) create mode 100644 packages/cli-common/src/workspaces.test.ts create mode 100644 packages/cli-common/src/workspaces.ts diff --git a/packages/cli-common/report.api.md b/packages/cli-common/report.api.md index ec146f78c4..324b28880b 100644 --- a/packages/cli-common/report.api.md +++ b/packages/cli-common/report.api.md @@ -24,7 +24,7 @@ export class ExitCodeError extends CustomErrorBase { export function findPaths(searchDir: string): Paths; // @public -export function getWorkspacesPatterns(pkgJson: any): string[]; +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 146af314cd..e142030b88 100644 --- a/packages/cli-common/src/index.ts +++ b/packages/cli-common/src/index.ts @@ -20,7 +20,8 @@ * @packageDocumentation */ -export { findPaths, getWorkspacesPatterns, BACKSTAGE_JSON } from './paths'; +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.test.ts b/packages/cli-common/src/paths.test.ts index 6ade904e12..f3d7b9b873 100644 --- a/packages/cli-common/src/paths.test.ts +++ b/packages/cli-common/src/paths.test.ts @@ -98,7 +98,9 @@ describe('paths', () => { }); it('findPaths should find workspace root with object', () => { - jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: { packages: [] } }); + jest + .spyOn(JSON, 'parse') + .mockReturnValue({ workspaces: { packages: ['packages/*'] } }); jest.spyOn(process, 'cwd').mockReturnValue(__dirname); const paths = findPaths(__dirname); @@ -110,7 +112,7 @@ describe('paths', () => { }); it('findPaths should find workspace root with array', () => { - jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: [] }); + jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: ['packages/*'] }); jest.spyOn(process, 'cwd').mockReturnValue(__dirname); const paths = findPaths(__dirname); diff --git a/packages/cli-common/src/paths.ts b/packages/cli-common/src/paths.ts index 557af7ccca..efbb05c7bb 100644 --- a/packages/cli-common/src/paths.ts +++ b/packages/cli-common/src/paths.ts @@ -16,6 +16,7 @@ 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 @@ -107,25 +108,6 @@ 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. * diff --git a/packages/cli-common/src/workspaces.test.ts b/packages/cli-common/src/workspaces.test.ts new file mode 100644 index 0000000000..7725aa7e6a --- /dev/null +++ b/packages/cli-common/src/workspaces.test.ts @@ -0,0 +1,69 @@ +/* + * 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 new file mode 100644 index 0000000000..17fca511a4 --- /dev/null +++ b/packages/cli-common/src/workspaces.ts @@ -0,0 +1,63 @@ +/* + * 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/pacman/PackageManager.test.ts b/packages/cli-node/src/pacman/PackageManager.test.ts index fd7ace5bcb..e394d2ed60 100644 --- a/packages/cli-node/src/pacman/PackageManager.test.ts +++ b/packages/cli-node/src/pacman/PackageManager.test.ts @@ -48,7 +48,7 @@ describe('PackageManager', () => { 'package.json': JSON.stringify({ name: 'foo', workspaces: { - packages: [], + packages: ['packages/*'], }, }), }); @@ -59,7 +59,7 @@ describe('PackageManager', () => { mockDir.setContent({ 'package.json': JSON.stringify({ name: 'foo', - workspaces: [], + workspaces: ['packages/*'], }), }); await detectPackageManager(); From 0e5fd0fd27f2fb5bec9738f02862a4854210c22c Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Sun, 22 Feb 2026 17:00:06 +0100 Subject: [PATCH 4/6] Revert most changes Signed-off-by: Gabriel Dugny --- .changeset/slick-dogs-pay.md | 1 - packages/cli-common/report.api.md | 3 - packages/cli-common/src/index.ts | 1 - packages/cli-common/src/paths.ts | 3 +- packages/cli-common/src/workspaces.test.ts | 69 ------------------- packages/cli-common/src/workspaces.ts | 63 ----------------- packages/cli-node/src/monorepo/isMonoRepo.ts | 8 ++- .../src/pacman/PackageManager.test.ts | 6 +- .../cli-node/src/pacman/PackageManager.ts | 4 +- packages/cli-node/src/pacman/yarn/Yarn.ts | 6 +- packages/cli/config/jest.js | 6 +- 11 files changed, 18 insertions(+), 152 deletions(-) delete mode 100644 packages/cli-common/src/workspaces.test.ts delete mode 100644 packages/cli-common/src/workspaces.ts 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, { From bc6c024a4ce0bc4cced64f9a7f83403c67ff7523 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Sun, 22 Feb 2026 17:26:35 +0100 Subject: [PATCH 5/6] chore: remove PackageManager getMonorepoPackages Unused, prefer @manypkg/get-packages's getPackages instead. Signed-off-by: Gabriel Dugny --- .../cli-node/src/pacman/PackageManager.ts | 6 -- .../cli-node/src/pacman/yarn/Yarn.test.ts | 68 ------------------- packages/cli-node/src/pacman/yarn/Yarn.ts | 15 ---- 3 files changed, 89 deletions(-) delete mode 100644 packages/cli-node/src/pacman/yarn/Yarn.test.ts diff --git a/packages/cli-node/src/pacman/PackageManager.ts b/packages/cli-node/src/pacman/PackageManager.ts index 44c0849705..0029652544 100644 --- a/packages/cli-node/src/pacman/PackageManager.ts +++ b/packages/cli-node/src/pacman/PackageManager.ts @@ -49,12 +49,6 @@ export interface PackageManager { /** The file name of the lockfile used by the package manager. */ lockfileName(): string; - /** - * If this repo is a monorepo, returns the patterns specified by the package - * manager's monorepo configuration. Does not attempt to resolve any globs. - */ - getMonorepoPackages(): Promise; - /** Uses the package manager to run a command in the repo. */ run(args: string[], options?: RunOptions): Promise; diff --git a/packages/cli-node/src/pacman/yarn/Yarn.test.ts b/packages/cli-node/src/pacman/yarn/Yarn.test.ts deleted file mode 100644 index f13b5a0296..0000000000 --- a/packages/cli-node/src/pacman/yarn/Yarn.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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. - */ - -import { createMockDirectory } from '@backstage/backend-test-utils'; -import { Yarn } from './Yarn'; - -const mockDir = createMockDirectory(); - -jest.mock('../../paths', () => ({ - ...jest.requireActual('../../paths'), - paths: { resolveTargetRoot: (...args: string[]) => mockDir.resolve(...args) }, -})); - -const yarnClassic = new Yarn({ version: '1.0.0', codename: 'classic' }); -const yarnBerry = new Yarn({ version: '3.0.0', codename: 'berry' }); -const allYarnVersions = [yarnClassic, yarnBerry]; - -describe('Yarn', () => { - describe.each(allYarnVersions)('%s.getMonorepoPackages', yarn => { - it('should detect a monorepo with workspaces.packages field', async () => { - mockDir.setContent({ - 'package.json': JSON.stringify({ - name: 'foo', - workspaces: { - packages: ['packages/*'], - }, - }), - }); - 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({ - 'package.json': JSON.stringify({ - name: 'foo', - }), - }); - await expect(yarn.getMonorepoPackages()).resolves.toEqual([]); - }); - - it('should return false if package.json is missing', async () => { - mockDir.setContent({}); - await expect(yarn.getMonorepoPackages()).resolves.toEqual([]); - }); - }); -}); diff --git a/packages/cli-node/src/pacman/yarn/Yarn.ts b/packages/cli-node/src/pacman/yarn/Yarn.ts index 9c242c90da..7aee7bdebf 100644 --- a/packages/cli-node/src/pacman/yarn/Yarn.ts +++ b/packages/cli-node/src/pacman/yarn/Yarn.ts @@ -22,8 +22,6 @@ import { import { PackageInfo, PackageManager } from '../PackageManager'; import { Lockfile } from '../Lockfile'; import { YarnVersion } from './types'; -import fs from 'fs-extra'; -import { paths } from '../../paths'; import { run, runOutput, RunOptions } from '@backstage/cli-common'; export class Yarn implements PackageManager { @@ -46,19 +44,6 @@ export class Yarn implements PackageManager { return 'yarn.lock'; } - async getMonorepoPackages() { - const rootPackageJsonPath = paths.resolveTargetRoot('package.json'); - try { - const pkg = await fs.readJson(rootPackageJsonPath); - const workspaces = pkg?.workspaces; - return Array.isArray(workspaces) - ? workspaces - : workspaces?.packages ?? []; - } catch (error) { - return []; - } - } - async pack(out: string, packageDir: string) { const outArg = this.yarnVersion.codename === 'classic' ? '--filename' : '--out'; From 37a309e7e20afd33e5ec1bac2553ca5b2b3da9a8 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Fri, 27 Feb 2026 12:04:16 +0100 Subject: [PATCH 6/6] Update .changeset/slow-items-pull.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Gabriel Dugny --- .changeset/slow-items-pull.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/slow-items-pull.md b/.changeset/slow-items-pull.md index 50f4897fa5..e3fe6e26c5 100644 --- a/.changeset/slow-items-pull.md +++ b/.changeset/slow-items-pull.md @@ -2,7 +2,7 @@ '@backstage/create-app': patch --- -Replace `package.json` deprecated `workspaces.packages` by `workspaces` +Replace deprecated `workspaces.packages` with `workspaces` in `package.json` This change is **not** required, but you can edit your main `package.json`, to fit the more modern & more common pattern: