From 05c4eae7de30f5f09f285f54165d7f28ed829549 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Sep 2022 15:03:17 +0200 Subject: [PATCH 1/5] cli: add isMonoRepo helper Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/monorepo/isMonoRepo.ts | 28 +++++++++++ .../cli/src/lib/monorepo/isMonorepo.test.ts | 50 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 packages/cli/src/lib/monorepo/isMonoRepo.ts create mode 100644 packages/cli/src/lib/monorepo/isMonorepo.test.ts diff --git a/packages/cli/src/lib/monorepo/isMonoRepo.ts b/packages/cli/src/lib/monorepo/isMonoRepo.ts new file mode 100644 index 0000000000..9b1ab93762 --- /dev/null +++ b/packages/cli/src/lib/monorepo/isMonoRepo.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2022 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 { paths } from '../paths'; +import fs from 'fs-extra'; + +export async function isMonoRepo(): Promise { + const rootPackageJsonPath = paths.resolveTargetRoot('package.json'); + try { + const pkg = await fs.readJson(rootPackageJsonPath); + return Boolean(pkg?.workspaces?.packages); + } catch (error) { + return false; + } +} diff --git a/packages/cli/src/lib/monorepo/isMonorepo.test.ts b/packages/cli/src/lib/monorepo/isMonorepo.test.ts new file mode 100644 index 0000000000..eb466a6ac8 --- /dev/null +++ b/packages/cli/src/lib/monorepo/isMonorepo.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2022 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 { isMonoRepo } from './isMonoRepo'; +import mockFs from 'mock-fs'; + +describe('isMonoRepo', () => { + afterEach(() => { + mockFs.restore(); + }); + + it('should detect a monorepo', async () => { + mockFs({ + 'package.json': JSON.stringify({ + name: 'foo', + workspaces: { + packages: ['packages/*'], + }, + }), + }); + await expect(isMonoRepo()).resolves.toBe(true); + }); + + it('should detect a non- monorepo', async () => { + mockFs({ + 'package.json': JSON.stringify({ + name: 'foo', + }), + }); + await expect(isMonoRepo()).resolves.toBe(false); + }); + + it('should return false if package.json is missing', async () => { + mockFs({}); + await expect(isMonoRepo()).resolves.toBe(false); + }); +}); From 00747a80734892445ac339b0489ae425cd0af34c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Sep 2022 16:40:26 +0200 Subject: [PATCH 2/5] cli: refactor to use isMonoRepo helper Signed-off-by: Patrik Oldsberg --- .../src/commands/create-plugin/createPlugin.ts | 1 + packages/cli/src/commands/new/new.ts | 18 ++---------------- .../cli/src/lib/new/factories/common/tasks.ts | 1 + packages/cli/src/lib/tasks.test.ts | 1 + packages/cli/src/lib/tasks.ts | 3 +-- 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index a12060bbd2..7fcb0aef1a 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -301,6 +301,7 @@ export default async (opts: OptionValues) => { npmRegistry, }, createPackageVersionProvider(lockfile), + isMonoRepo, ); Task.section('Moving to final location'); diff --git a/packages/cli/src/commands/new/new.ts b/packages/cli/src/commands/new/new.ts index b0d30c0415..a5f8f6763f 100644 --- a/packages/cli/src/commands/new/new.ts +++ b/packages/cli/src/commands/new/new.ts @@ -22,6 +22,7 @@ import { FactoryRegistry } from '../../lib/new/FactoryRegistry'; import { paths } from '../../lib/paths'; import { assertError } from '@backstage/errors'; import { Task } from '../../lib/tasks'; +import { isMonoRepo } from '../../lib/monorepo/isMonoRepo'; function parseOptions(optionStrings: string[]): Record { const options: Record = {}; @@ -49,21 +50,6 @@ export default async (opts: OptionValues) => { providedOptions, ); - let isMonoRepo = false; - try { - const rootPackageJson = await fs.readJson( - paths.resolveTargetRoot('package.json'), - ); - if (rootPackageJson.workspaces) { - isMonoRepo = true; - } - } catch (error) { - assertError(error); - if (error.code !== 'ENOENT') { - throw error; - } - } - let defaultVersion = '0.1.0'; try { const rootLernaJson = await fs.readJson( @@ -89,7 +75,7 @@ export default async (opts: OptionValues) => { let modified = false; try { await factory.create(options, { - isMonoRepo, + isMonoRepo: await isMonoRepo(), defaultVersion, scope: opts.scope?.replace(/^@/, ''), npmRegistry: opts.npmRegistry, diff --git a/packages/cli/src/lib/new/factories/common/tasks.ts b/packages/cli/src/lib/new/factories/common/tasks.ts index 93af1e2b00..99ffabfb24 100644 --- a/packages/cli/src/lib/new/factories/common/tasks.ts +++ b/packages/cli/src/lib/new/factories/common/tasks.ts @@ -62,6 +62,7 @@ export async function executePluginPackageTemplate( tempDir, options.values, createPackageVersionProvider(lockfile), + ctx.isMonoRepo, ); // Format package.json if it exists diff --git a/packages/cli/src/lib/tasks.test.ts b/packages/cli/src/lib/tasks.test.ts index 4d9af05c4b..fb1f38536f 100644 --- a/packages/cli/src/lib/tasks.test.ts +++ b/packages/cli/src/lib/tasks.test.ts @@ -53,6 +53,7 @@ describe('templatingTask', () => { pluginVersion: '0.0.0', }, () => '^0.1.2', + true, ); await expect( diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index c84eed435e..851f962c62 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -22,7 +22,6 @@ import { promisify } from 'util'; import { basename, dirname } from 'path'; import recursive from 'recursive-readdir'; import { exec as execCb } from 'child_process'; -import { paths } from './paths'; import { assertError } from '@backstage/errors'; const exec = promisify(execCb); @@ -102,11 +101,11 @@ export async function templatingTask( destinationDir: string, context: any, versionProvider: (name: string, versionHint?: string) => string, + isMonoRepo: boolean, ) { const files = await recursive(templateDir).catch(error => { throw new Error(`Failed to read template directory: ${error.message}`); }); - const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json')); for (const file of files) { const destinationFile = file.replace(templateDir, destinationDir); From 0e46432621cb7cd8e661cadea4617590e9165313 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Sep 2022 16:41:57 +0200 Subject: [PATCH 3/5] cli: add baseVersion option to new command Signed-off-by: Patrik Oldsberg --- package.json | 2 +- packages/cli/cli-report.md | 1 + packages/cli/src/commands/index.ts | 4 ++++ packages/cli/src/commands/new/new.ts | 20 +++++++++----------- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 77ee1dccc5..4edd364147 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "lint:all": "backstage-cli repo lint", "lint:type-deps": "node scripts/check-type-dependencies.js", "docker-build": "yarn tsc && yarn workspace example-backend build --build-dependencies && yarn workspace example-backend build-image", - "new": "backstage-cli new --scope backstage --no-private", + "new": "backstage-cli new --scope backstage --baseVersion 0.0.0 --no-private", "create-plugin": "echo \"use 'yarn new' instead\"", "release": "node scripts/prepare-release.js && changeset version && yarn diff --yes && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' '.changeset/*.json' && yarn install --no-immutable", "prettier:check": "prettier --check .", diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index 1c86c8723c..8b71e8ca17 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -179,6 +179,7 @@ Options: --option = --scope --npm-registry + --baseVersion --no-private -h, --help ``` diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 0af1674b6c..f2257f32a0 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -218,6 +218,10 @@ export function registerCommands(program: Command) { '--npm-registry ', 'The package registry to use for new packages', ) + .option( + '--baseVersion ', + 'The version to use for any new packages (default: 0.1.0)', + ) .option('--no-private', 'Do not mark new packages as private') .action(lazy(() => import('./new/new').then(m => m.default))); diff --git a/packages/cli/src/commands/new/new.ts b/packages/cli/src/commands/new/new.ts index a5f8f6763f..8aaa671614 100644 --- a/packages/cli/src/commands/new/new.ts +++ b/packages/cli/src/commands/new/new.ts @@ -51,17 +51,15 @@ export default async (opts: OptionValues) => { ); let defaultVersion = '0.1.0'; - try { - const rootLernaJson = await fs.readJson( - paths.resolveTargetRoot('lerna.json'), - ); - if (rootLernaJson.version) { - defaultVersion = rootLernaJson.version; - } - } catch (error) { - assertError(error); - if (error.code !== 'ENOENT') { - throw error; + if (opts.baseVersion) { + defaultVersion = opts.baseVersion; + } else { + const lernaVersion = await fs + .readJson(paths.resolveTargetRoot('lerna.json')) + .then(pkg => pkg.version) + .catch(() => undefined); + if (lernaVersion) { + defaultVersion = lernaVersion; } } From 0f0058673e76ef915794fb79079eaebdc9928fd3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Sep 2022 17:13:34 +0200 Subject: [PATCH 4/5] cli: stop using lerna to execute build scripts when building dist workspace Signed-off-by: Patrik Oldsberg --- .../src/lib/packager/createDistWorkspace.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/lib/packager/createDistWorkspace.ts b/packages/cli/src/lib/packager/createDistWorkspace.ts index bce6c5c14c..53e00ab6d9 100644 --- a/packages/cli/src/lib/packager/createDistWorkspace.ts +++ b/packages/cli/src/lib/packager/createDistWorkspace.ts @@ -39,6 +39,7 @@ import { } from '../builder'; import { copyPackageDist } from './copyPackageDist'; import { getRoleInfo } from '../role'; +import { runParallelWorkers } from '../parallel'; // These packages aren't safe to pack in parallel since the CLI depends on them const UNSAFE_PACKAGES = [ @@ -88,6 +89,14 @@ type Options = { skeleton?: 'skeleton.tar' | 'skeleton.tar.gz'; }; +function prefixLogFunc(prefix: string, out: 'stdout' | 'stderr') { + return (data: Buffer) => { + for (const line of data.toString('utf8').split(/\r?\n/)) { + process[out].write(`${prefix} ${line}\n`); + } + }; +} + /** * Uses `yarn pack` to package local packages and unpacks them into a dist workspace. * The target workspace will end up containing dist version of each package and @@ -124,7 +133,7 @@ export async function createDistWorkspace( ); const standardBuilds = new Array(); - const customBuild = new Array(); + const customBuild = new Array<{ dir: string; name: string }>(); for (const pkg of packages) { if (!toBuild.has(pkg.packageJson.name)) { @@ -135,13 +144,13 @@ export async function createDistWorkspace( console.warn( `Building ${pkg.packageJson.name} separately because it has no role`, ); - customBuild.push(pkg.packageJson.name); + customBuild.push({ dir: pkg.dir, name: pkg.packageJson.name }); continue; } const buildScript = pkg.packageJson.scripts?.build; if (!buildScript) { - customBuild.push(pkg.packageJson.name); + customBuild.push({ dir: pkg.dir, name: pkg.packageJson.name }); continue; } @@ -149,7 +158,7 @@ export async function createDistWorkspace( console.warn( `Building ${pkg.packageJson.name} separately because it has a custom build script, '${buildScript}'`, ); - customBuild.push(pkg.packageJson.name); + customBuild.push({ dir: pkg.dir, name: pkg.packageJson.name }); continue; } @@ -157,7 +166,7 @@ export async function createDistWorkspace( console.warn( `Building ${pkg.packageJson.name} separately because it is a bundled package`, ); - customBuild.push(pkg.packageJson.name); + customBuild.push({ dir: pkg.dir, name: pkg.packageJson.name }); continue; } @@ -181,14 +190,15 @@ export async function createDistWorkspace( await buildPackages(standardBuilds); if (customBuild.length > 0) { - const scopeArgs = customBuild.flatMap(name => ['--scope', name]); - const lernaArgs = - options.parallelism && Number.isInteger(options.parallelism) - ? ['--concurrency', options.parallelism.toString()] - : []; - - await run('yarn', ['lerna', ...lernaArgs, 'run', ...scopeArgs, 'build'], { - cwd: paths.targetRoot, + await runParallelWorkers({ + items: customBuild, + worker: async ({ name, dir }) => { + await run('yarn', ['run', 'build'], { + cwd: dir, + stdoutLogFunc: prefixLogFunc(`${name}: `, 'stdout'), + stderrLogFunc: prefixLogFunc(`${name}: `, 'stderr'), + }); + }, }); } } From 1fd4f2746f9097da578ef1d26bb884bf664523cc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Sep 2022 17:17:19 +0200 Subject: [PATCH 5/5] changesets: added changeset for removal of Lerna dependency Signed-off-by: Patrik Oldsberg --- .changeset/purple-wolves-confess.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/purple-wolves-confess.md diff --git a/.changeset/purple-wolves-confess.md b/.changeset/purple-wolves-confess.md new file mode 100644 index 0000000000..33333c1a1b --- /dev/null +++ b/.changeset/purple-wolves-confess.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Removed internal dependencies on Lerna. It is now no longer necessary to have Lerna installed in a project to use all features of the Backstage CLI.