From 1f8b01effe140045ae47e82de07154239f50911c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Apr 2020 14:36:24 +0200 Subject: [PATCH] packages/cli: add utility for resolving paths and use everywhere --- .../cli/src/commands/build-cache/cache.ts | 5 +- .../cli/src/commands/build-cache/options.ts | 8 +- .../cli/src/commands/create-app/createApp.ts | 17 ++- .../commands/create-plugin/createPlugin.ts | 26 ++--- packages/cli/src/commands/testCommand.ts | 8 +- packages/cli/src/commands/watch-deps/index.ts | 4 +- .../cli/src/commands/watch-deps/packages.ts | 4 +- packages/cli/src/helpers/paths.ts | 106 ++++++++++++++++++ packages/cli/src/index.ts | 9 +- 9 files changed, 140 insertions(+), 47 deletions(-) create mode 100644 packages/cli/src/helpers/paths.ts diff --git a/packages/cli/src/commands/build-cache/cache.ts b/packages/cli/src/commands/build-cache/cache.ts index f52e081a3f..464071c95f 100644 --- a/packages/cli/src/commands/build-cache/cache.ts +++ b/packages/cli/src/commands/build-cache/cache.ts @@ -19,6 +19,7 @@ import { resolve as resolvePath, relative as relativePath } from 'path'; import { runPlain, runCheck } from '../../helpers/run'; import { Options } from './options'; import { extractArchive, createArchive } from './archive'; +import { paths } from '../../helpers/paths'; const INFO_FILE = '.backstage-build-cache'; @@ -58,8 +59,8 @@ type CacheInfo = { export class Cache { // Read the current cache state form the filesystem. static async read(options: Options) { - const repoPath = relativePath(options.repoRoot, process.cwd()); - const location = resolvePath(options.cacheDir, repoPath); + const relativePackagePath = relativePath(paths.targetRoot, paths.targetDir); + const location = resolvePath(options.cacheDir, relativePackagePath); const outputInfo = await readCacheInfo(options.output); const localKey = outputInfo?.key; diff --git a/packages/cli/src/commands/build-cache/options.ts b/packages/cli/src/commands/build-cache/options.ts index bad7d6c27e..ae5404bb9f 100644 --- a/packages/cli/src/commands/build-cache/options.ts +++ b/packages/cli/src/commands/build-cache/options.ts @@ -16,7 +16,7 @@ import { resolve as resolvePath } from 'path'; import { Command } from 'commander'; -import { runPlain } from '../../helpers/run'; +import { paths } from '../../helpers/paths'; const DEFAULT_MAX_ENTRIES = 10; @@ -25,13 +25,11 @@ export type Options = { output: string; cacheDir: string; maxCacheEntries: number; - repoRoot: string; }; export async function parseOptions(cmd: Command): Promise { - const repoRoot = await runPlain('git rev-parse --show-toplevel'); const argTransformer = (arg: string) => - resolvePath(arg.replace(//g, repoRoot).replace(/'/g, '')); + resolvePath(arg.replace(//g, paths.targetRoot).replace(/'/g, '')); const inputs = cmd.input.map(argTransformer) as string[]; if (inputs.length === 0) { @@ -43,5 +41,5 @@ export async function parseOptions(cmd: Command): Promise { ); const maxCacheEntries = Number(process.env.BACKSTAGE_CACHE_MAX_ENTRIES) || DEFAULT_MAX_ENTRIES; - return { inputs, output, cacheDir, repoRoot, maxCacheEntries }; + return { inputs, output, cacheDir, maxCacheEntries }; } diff --git a/packages/cli/src/commands/create-app/createApp.ts b/packages/cli/src/commands/create-app/createApp.ts index 71e8ec50e9..03f571fed5 100644 --- a/packages/cli/src/commands/create-app/createApp.ts +++ b/packages/cli/src/commands/create-app/createApp.ts @@ -15,20 +15,19 @@ */ import fs from 'fs-extra'; -import path from 'path'; import { promisify } from 'util'; import chalk from 'chalk'; import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; import { resolve as resolvePath } from 'path'; -import { realpathSync } from 'fs'; import os from 'os'; import { Task, templatingTask } from '../../helpers/tasks'; +import { paths } from '../../helpers/paths'; const exec = promisify(execCb); async function checkExists(rootDir: string, name: string) { await Task.forItem('checking', name, async () => { - const destination = path.join(rootDir, 'plugins', name); + const destination = resolvePath(rootDir, name); if (await fs.pathExists(destination)) { const existing = chalk.cyan(destination.replace(`${rootDir}/`, '')); @@ -106,19 +105,17 @@ export default async () => { ]; const answers: Answers = await inquirer.prompt(questions); - const rootDir = realpathSync(process.cwd()); - const cliPackage = resolvePath(__dirname, '../../..'); - const templateDir = resolvePath(cliPackage, 'templates', 'default-app'); - const tempDir = path.join(os.tmpdir(), answers.name); - const appDir = path.join(rootDir, answers.name); - const version = require(resolvePath(cliPackage, 'package.json')).version; + const templateDir = paths.resolveOwn('templates/default-app'); + const tempDir = resolvePath(os.tmpdir(), answers.name); + const appDir = resolvePath(paths.targetDir, answers.name); + const version = require(paths.resolveOwn('package.json')).version; Task.log(); Task.log('Creating the app...'); try { Task.section('Checking if the directory is available'); - await checkExists(rootDir, answers.name); + await checkExists(paths.targetDir, answers.name); Task.section('Creating a temporary app directory'); await createTemporaryAppFolder(tempDir); diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 7802f402f9..027a53f2b4 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -15,7 +15,6 @@ */ import fs from 'fs-extra'; -import path from 'path'; import { promisify } from 'util'; import chalk from 'chalk'; import inquirer, { Answers, Question } from 'inquirer'; @@ -27,12 +26,13 @@ import { addCodeownersEntry, getCodeownersFilePath, } from './lib/codeowners'; +import { paths } from '../../helpers/paths'; import { Task, templatingTask } from '../../helpers/tasks'; const exec = promisify(execCb); async function checkExists(rootDir: string, id: string) { await Task.forItem('checking', id, async () => { - const destination = path.join(rootDir, 'plugins', id); + const destination = resolvePath(rootDir, 'plugins', id); if (await fs.pathExists(destination)) { const existing = chalk.cyan(destination.replace(`${rootDir}/`, '')); @@ -90,7 +90,7 @@ export async function addPluginDependencyToApp( ) { const pluginPackage = `@backstage/plugin-${pluginName}`; const packageFilePath = 'packages/app/package.json'; - const packageFile = path.join(rootDir, packageFilePath); + const packageFile = resolvePath(rootDir, packageFilePath); await Task.forItem('processing', packageFilePath, async () => { const packageFileContent = await fs.readFile(packageFile, 'utf-8'); @@ -173,8 +173,7 @@ export async function movePlugin( } export default async () => { - const rootDir = await fs.realpath(process.cwd()); - const codeownersPath = await getCodeownersFilePath(rootDir); + const codeownersPath = await getCodeownersFilePath(paths.targetRoot); const questions: Question[] = [ { @@ -220,12 +219,11 @@ export default async () => { const answers: Answers = await inquirer.prompt(questions); - const appPackage = resolvePath(rootDir, 'packages', 'app'); - const cliPackage = resolvePath(__dirname, '..', '..', '..'); - const templateDir = resolvePath(cliPackage, 'templates', 'default-plugin'); - const tempDir = path.join(os.tmpdir(), answers.id); - const pluginDir = path.join(rootDir, 'plugins', answers.id); - const version = require(resolvePath(cliPackage, 'package.json')).version; + const appPackage = paths.resolveTargetRoot('packages/app'); + const templateDir = paths.resolveOwn('templates/default-plugin'); + const tempDir = resolvePath(os.tmpdir(), answers.id); + const pluginDir = paths.resolveTargetRoot('plugins', answers.id); + const version = require(paths.resolveOwn('package.json')).version; const ownerIds = parseOwnerIds(answers.owner); Task.log(); @@ -233,7 +231,7 @@ export default async () => { try { Task.section('Checking if the plugin ID is available'); - await checkExists(rootDir, answers.id); + await checkExists(paths.targetRoot, answers.id); Task.section('Creating a temporary plugin directory'); await createTemporaryPluginFolder(tempDir); @@ -249,10 +247,10 @@ export default async () => { if (await fs.pathExists(appPackage)) { Task.section('Adding plugin as dependency in app'); - await addPluginDependencyToApp(rootDir, answers.id, version); + await addPluginDependencyToApp(paths.targetRoot, answers.id, version); Task.section('Import plugin in app'); - await addPluginToApp(rootDir, answers.id); + await addPluginToApp(paths.targetRoot, answers.id); } if (ownerIds && ownerIds.length) { diff --git a/packages/cli/src/commands/testCommand.ts b/packages/cli/src/commands/testCommand.ts index f632670184..3fc87b158b 100644 --- a/packages/cli/src/commands/testCommand.ts +++ b/packages/cli/src/commands/testCommand.ts @@ -15,15 +15,11 @@ */ import { Command } from 'commander'; -import { resolve as resolvePath } from 'path'; import { run } from '../helpers/run'; +import { paths } from '../helpers/paths'; export default async (cmd: Command) => { - const args = [ - 'test', - '--config', - resolvePath(__dirname, '../../config/jest.js'), - ]; + const args = ['test', '--config', paths.resolveOwn('config/jest.js')]; if (cmd.watch) { args.push('--watch'); diff --git a/packages/cli/src/commands/watch-deps/index.ts b/packages/cli/src/commands/watch-deps/index.ts index 059c2a7b5e..7ffa67ed7a 100644 --- a/packages/cli/src/commands/watch-deps/index.ts +++ b/packages/cli/src/commands/watch-deps/index.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { resolve as resolvePath } from 'path'; import chalk from 'chalk'; import { createLoggerFactory } from './logger'; @@ -23,6 +22,7 @@ import { startWatcher, startPackageWatcher } from './watcher'; import { startCompiler } from './compiler'; import { startChild } from './child'; import { waitForExit } from '../../helpers/run'; +import { paths } from '../../helpers/paths'; const PACKAGE_BLACKLIST = [ // We never want to watch for changes in the cli, but all packages will depend on it. @@ -34,7 +34,7 @@ const WATCH_LOCATIONS = ['package.json', 'src', 'assets']; // Start watching for dependency changes. // The returned promise resolves when watchers have started for all current dependencies. export async function watchDeps() { - const localPackagePath = resolvePath('package.json'); + const localPackagePath = paths.resolveTarget('package.json'); // Rotate through different prefix colors to make it easier to differenciate between different deps const logFactory = createLoggerFactory([ diff --git a/packages/cli/src/commands/watch-deps/packages.ts b/packages/cli/src/commands/watch-deps/packages.ts index 2e778e5a57..e80de3651d 100644 --- a/packages/cli/src/commands/watch-deps/packages.ts +++ b/packages/cli/src/commands/watch-deps/packages.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { resolve as resolvePath } from 'path'; import fs from 'fs'; import { promisify } from 'util'; +import { paths } from '../../helpers/paths'; const readFile = promisify(fs.readFile); @@ -34,7 +34,7 @@ export async function findAllDeps( rootPackageName: string, blacklist: string[], ): Promise { - const project = new LernaProject(resolvePath('.')); + const project = new LernaProject(paths.targetDir); const packages = await project.getPackages(); const graph = new PackageGraph(packages); diff --git a/packages/cli/src/helpers/paths.ts b/packages/cli/src/helpers/paths.ts new file mode 100644 index 0000000000..f49bfa9689 --- /dev/null +++ b/packages/cli/src/helpers/paths.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 fs from 'fs-extra'; +import { dirname, resolve as resolvePath } from 'path'; + +export type ResolveFunc = (...paths: string[]) => string; + +// Common paths and resolve functions used by the cli. +// Currently assumes it is being executed within a monorepo. +export type Paths = { + // Root dir of the cli itself, containing package.json + ownDir: string; + + // The location of the app that the cli is being executed in + targetDir: string; + + // The monorepo root package of the app that the cli is being executed in. + targetRoot: string; + + // Resolve a path relative to own repo + resolveOwn: ResolveFunc; + + // Resolve a path relative to the app + resolveTarget: ResolveFunc; + + // Resolve a path relative to the app repo root + resolveTargetRoot: ResolveFunc; +}; + +// Looks for a package.json that has name: "root" to identify the root of the monorepo +export function findRootPath(topPath: string): string { + let path = topPath; + + // Some sanity check to avoid infinite loop + for (let i = 0; i < 1000; i++) { + const packagePath = resolvePath(path, 'package.json'); + const exists = fs.pathExistsSync(packagePath); + if (exists) { + try { + const contents = fs.readFileSync(packagePath, 'utf8'); + const data = JSON.parse(contents); + if (data.name === 'root') { + return path; + } + } catch (error) { + throw new Error( + `Failed to parse package.json file while searching for root, ${error}`, + ); + } + } + + const newPath = dirname(path); + if (newPath === path) { + throw new Error( + `No package.json with name "root" found as a parent of ${topPath}`, + ); + } + path = newPath; + } + + throw new Error( + `Iteration limit reached when searching for root package.json at ${topPath}`, + ); +} + +export function findPaths(): Paths { + const ownDir = resolvePath(__dirname, '../..'); + const targetDir = fs.realpathSync(process.cwd()); + + // We're not always running in a monorepo, so we lazy init this to only crash commands + // that require a monorepo when we're not in one. + let targetRoot = ''; + const getTargetRoot = () => { + if (!targetRoot) { + targetRoot = findRootPath(targetDir); + } + return targetRoot; + }; + + return { + ownDir, + targetDir, + get targetRoot() { + return getTargetRoot(); + }, + resolveOwn: (...paths) => resolvePath(ownDir, ...paths), + resolveTarget: (...paths) => resolvePath(targetDir, ...paths), + resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths), + }; +} + +export const paths = findPaths(); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 251252fe43..cffc24d902 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -16,8 +16,6 @@ import program from 'commander'; import chalk from 'chalk'; -import fs from 'fs'; -import { resolve as resolvePath } from 'path'; import createAppCommand from './commands/create-app/createApp'; import createPluginCommand from './commands/create-plugin/createPlugin'; import watch from './commands/watch-deps'; @@ -29,13 +27,12 @@ import appServe from './commands/app/serve'; import pluginBuild from './commands/plugin/build'; import pluginServe from './commands/plugin/serve'; import { exitWithError } from './helpers/errors'; +import { paths } from './helpers/paths'; const main = (argv: string[]) => { - const packageJson = JSON.parse( - fs.readFileSync(resolvePath(__dirname, '../package.json'), 'utf-8'), - ); + const version = require(paths.resolveOwn('package.json')).version; - program.name('backstage-cli').version(packageJson.version ?? '0.0.0'); + program.name('backstage-cli').version(version); program .command('create-app')