Merge pull request #1813 from spotify/rugvip/cli-common

cli: separate out path discovery logic that's common to CLIs, backend and config
This commit is contained in:
Patrik Oldsberg
2020-08-05 12:10:25 +02:00
committed by GitHub
36 changed files with 266 additions and 470 deletions
+2
View File
@@ -29,7 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.16",
"@backstage/config": "^0.1.1-alpha.16",
"@backstage/config-loader": "^0.1.1-alpha.16",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
@@ -14,13 +14,17 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import { paths } from './paths';
import { findPaths } from '@backstage/cli-common';
import { loadConfig } from '@backstage/config-loader';
export function findVersion() {
const pkgContent = fs.readFileSync(paths.resolveOwn('package.json'), 'utf8');
return JSON.parse(pkgContent).version;
/**
* Load configuration for a Backend
*/
export async function loadBackendConfig() {
const paths = findPaths(__dirname);
const configs = await loadConfig({
rootPath: paths.targetRoot,
shouldReadSecrets: true,
});
return configs;
}
export const version = findVersion();
export const isDev = fs.pathExistsSync(paths.resolveOwn('src'));
+1
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
export * from './config';
export * from './errors';
export * from './logging';
export * from './middleware';
-1
View File
@@ -21,7 +21,6 @@
"@backstage/backend-common": "^0.1.1-alpha.16",
"@backstage/catalog-model": "^0.1.1-alpha.16",
"@backstage/config": "^0.1.1-alpha.16",
"@backstage/config-loader": "^0.1.1-alpha.16",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.16",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.16",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.16",
+2 -2
View File
@@ -24,11 +24,11 @@
import {
createServiceBuilder,
loadBackendConfig,
getRootLogger,
useHotMemoize,
} from '@backstage/backend-common';
import { ConfigReader, AppConfig } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import knex, { PgConnectionConfig } from 'knex';
import healthcheck from './plugins/healthcheck';
import auth from './plugins/auth';
@@ -85,7 +85,7 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
}
async function main() {
const configs = await loadConfig({ shouldReadSecrets: true });
const configs = await loadBackendConfig();
const configReader = ConfigReader.fromConfigs(configs);
const createEnv = makeCreateEnv(configs);
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+12
View File
@@ -0,0 +1,12 @@
# @backstage/cli-common
This package provides config loading functionality used by the backend, and CLI.
## Installation
Do not install this package directly, it is an internal package used by [@backstage/cli](https://www.npmjs.com/package/@backstage/cli), and [@backstage/create-app](https://www.npmjs.com/package/@backstage/create-app). Depend on either of those instead.
## Documentation
- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md)
- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md)
+39
View File
@@ -0,0 +1,39 @@
{
"name": "@backstage/cli-common",
"description": "Common functionality used by cli, backend, and create-app",
"version": "0.1.1-alpha.16",
"private": false,
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/spotify/backstage",
"directory": "packages/cli-common"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"scripts": {
"build": "backstage-cli build --outputs cjs,types",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.16",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0"
},
"files": [
"dist"
]
}
@@ -14,11 +14,5 @@
* limitations under the License.
*/
import { findRootPath } from './paths';
describe('findRootPath', () => {
it('should find root path', () => {
const rootPath = findRootPath(process.cwd());
expect(typeof rootPath).toBe('string');
});
});
export { findPaths } from './paths';
export type { Paths } from './paths';
+92
View File
@@ -0,0 +1,92 @@
/*
* 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 { resolve as resolvePath } from 'path';
import { findPaths, findRootPath, findOwnDir, findOwnRootDir } from './paths';
describe('paths', () => {
it('findOwnDir and findOwnRootDir should find owns paths', () => {
const dir = findOwnDir(__dirname);
const root = findOwnRootDir(dir);
expect(dir).toBe(resolvePath(__dirname, '..'));
expect(root).toBe(resolvePath(__dirname, '../../..'));
});
it('findRootPath should find a root path', () => {
expect(findRootPath(__dirname, () => true)).toBe(
resolvePath(__dirname, '..'),
);
expect(findRootPath(__dirname, () => false)).toBeUndefined();
expect(
findRootPath(
__dirname,
path => path !== resolvePath(__dirname, '../package.json'),
),
).toBe(resolvePath(__dirname, '../../..'));
});
it('findPaths should find package paths', () => {
const dir = resolvePath(__dirname, '..');
const root = resolvePath(__dirname, '../../..');
const paths = findPaths(__dirname);
expect(paths.ownDir).toBe(dir);
expect(paths.ownRoot).toBe(root);
expect(paths.resolveOwn('./derp.txt')).toBe(resolvePath(dir, 'derp.txt'));
expect(paths.resolveOwnRoot('./derp.txt')).toBe(
resolvePath(root, 'derp.txt'),
);
expect(paths.targetDir).toBe(dir);
expect(paths.targetRoot).toBe(root);
expect(paths.resolveTarget('./derp.txt')).toBe(
resolvePath(dir, 'derp.txt'),
);
expect(paths.resolveTargetRoot('./derp.txt')).toBe(
resolvePath(root, 'derp.txt'),
);
});
it('findPaths should find mocked package paths', () => {
const mockCwd = resolvePath(__dirname, '../../core');
const mockDir = resolvePath(__dirname, '../../cli');
const root = resolvePath(__dirname, '../../..');
jest.spyOn(process, 'cwd').mockReturnValue(mockCwd);
const paths = findPaths(resolvePath(mockDir, 'src/lib'));
expect(paths.ownDir).toBe(mockDir);
expect(paths.ownRoot).toBe(root);
expect(paths.resolveOwn('./derp.txt')).toBe(
resolvePath(mockDir, 'derp.txt'),
);
expect(paths.resolveOwnRoot('./derp.txt')).toBe(
resolvePath(root, 'derp.txt'),
);
expect(paths.targetDir).toBe(mockCwd);
expect(paths.targetRoot).toBe(root);
expect(paths.resolveTarget('./derp.txt')).toBe(
resolvePath(mockCwd, 'derp.txt'),
);
expect(paths.resolveTargetRoot('./derp.txt')).toBe(
resolvePath(root, 'derp.txt'),
);
});
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import fs from 'fs';
import { dirname, resolve as resolvePath } from 'path';
export type ResolveFunc = (...paths: string[]) => string;
@@ -47,58 +47,47 @@ export type Paths = {
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;
// Looks for a package.json with a workspace config to identify the root of the monorepo
export function findRootPath(
searchDir: string,
filterFunc: (pkgJsonPath: string) => boolean,
): string | undefined {
let path = searchDir;
// 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 data = fs.readJsonSync(packagePath);
if (data.name === 'root' || data.name.includes('backstage-e2e')) {
return path;
}
} catch (error) {
throw new Error(
`Failed to parse package.json file while searching for root, ${error}`,
);
}
const exists = fs.existsSync(packagePath);
if (exists && filterFunc(packagePath)) {
return path;
}
const newPath = dirname(path);
if (newPath === path) {
throw new Error(
`No package.json with name "root" found as a parent of ${topPath}`,
);
return undefined;
}
path = newPath;
}
throw new Error(
`Iteration limit reached when searching for root package.json at ${topPath}`,
`Iteration limit reached when searching for root package.json at ${searchDir}`,
);
}
// Finds the root of the cli package itself
export function findOwnDir() {
// Known relative locations of package in dist/dev
const pathDist = '..';
const pathDev = '../..';
// Check the closest dir first
const pkgInDist = resolvePath(__dirname, pathDist, 'package.json');
const isDist = fs.pathExistsSync(pkgInDist);
const path = isDist ? pathDist : pathDev;
return resolvePath(__dirname, path);
// Finds the root of a given package
export function findOwnDir(searchDir: string) {
const path = findRootPath(searchDir, () => true);
if (!path) {
throw new Error(
`No package.json found while searching for package root of ${searchDir}`,
);
}
return path;
}
// Finds the root of the monorepo that the cli exists in. Only accessible when running inside Backstage repo.
export function findOwnRootPath(ownDir: string) {
const isLocal = fs.pathExistsSync(resolvePath(ownDir, 'src'));
// Finds the root of the monorepo that the package exists in. Only accessible when running inside Backstage repo.
export function findOwnRootDir(ownDir: string) {
const isLocal = fs.existsSync(resolvePath(ownDir, 'src'));
if (!isLocal) {
throw new Error(
'Tried to access monorepo package root dir outside of Backstage repository',
@@ -108,15 +97,22 @@ export function findOwnRootPath(ownDir: string) {
return resolvePath(ownDir, '../..');
}
export function findPaths(): Paths {
const ownDir = findOwnDir();
/**
* Find paths related to a package and its execution context.
*
* @example
*
* const paths = findPaths(__dirname)
*/
export function findPaths(searchDir: string): Paths {
const ownDir = findOwnDir(searchDir);
const targetDir = fs.realpathSync(process.cwd());
// Lazy load this as it will throw an error if we're not inside the Backstage repo.
let ownRoot = '';
const getOwnRoot = () => {
if (!ownRoot) {
ownRoot = findOwnRootPath(ownDir);
ownRoot = findOwnRootDir(ownDir);
}
return ownRoot;
};
@@ -126,7 +122,18 @@ export function findPaths(): Paths {
let targetRoot = '';
const getTargetRoot = () => {
if (!targetRoot) {
targetRoot = findRootPath(targetDir);
targetRoot =
findRootPath(targetDir, path => {
try {
const content = fs.readFileSync(path, 'utf8');
const data = JSON.parse(content);
return Boolean(data.workspaces?.packages);
} catch (error) {
throw new Error(
`Failed to parse package.json file while searching for root, ${error}`,
);
}
}) ?? targetDir; // We didn't find any root package.json, assume we're not in a monorepo
}
return targetRoot;
};
@@ -146,5 +153,3 @@ export function findPaths(): Paths {
resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths),
};
}
export const paths = findPaths();
+10 -6
View File
@@ -73,6 +73,8 @@ async function buildDistWorkspace(workspaceName, rootDir) {
'@backstage/core',
'@backstage/dev-utils',
'@backstage/test-utils',
// We don't use the backend itself, but want all of its dependencies
'example-backend',
]);
print('Pinning yarn version in workspace');
@@ -180,13 +182,15 @@ async function overrideModuleResolutions(appDir, workspaceDir) {
pkgJson.resolutions = pkgJson.resolutions || {};
pkgJson.dependencies = pkgJson.dependencies || {};
const packageNames = await fs.readdir(resolvePath(workspaceDir, 'packages'));
for (const name of packageNames) {
const pkgPath = joinPath('..', 'workspace', 'packages', name);
for (const dir of ['packages', 'plugins']) {
const packageNames = await fs.readdir(resolvePath(workspaceDir, dir));
for (const name of packageNames) {
const pkgPath = joinPath('..', 'workspace', dir, name);
pkgJson.dependencies[`@backstage/${name}`] = `file:${pkgPath}`;
pkgJson.resolutions[`@backstage/${name}`] = `file:${pkgPath}`;
delete pkgJson.devDependencies[`@backstage/${name}`];
pkgJson.dependencies[`@backstage/${name}`] = `file:${pkgPath}`;
pkgJson.resolutions[`@backstage/${name}`] = `file:${pkgPath}`;
delete pkgJson.devDependencies[`@backstage/${name}`];
}
}
fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
}
+1
View File
@@ -29,6 +29,7 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.16",
"@backstage/config": "^0.1.1-alpha.16",
"@backstage/config-loader": "^0.1.1-alpha.16",
"@hot-loader/react-dom": "^16.13.0",
+2 -1
View File
@@ -17,10 +17,11 @@
import { Command } from 'commander';
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { paths } from '../../lib/paths';
import { buildBundle } from '../../lib/bundler';
export default async (cmd: Command) => {
const appConfigs = await loadConfig();
const appConfigs = await loadConfig({ rootPath: paths.targetRoot });
await buildBundle({
entry: 'src/index',
statsJsonEnabled: cmd.stats,
+2 -1
View File
@@ -17,10 +17,11 @@
import { Command } from 'commander';
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { paths } from '../../lib/paths';
import { serveBundle } from '../../lib/bundler';
export default async (cmd: Command) => {
const appConfigs = await loadConfig();
const appConfigs = await loadConfig({ rootPath: paths.targetRoot });
const waitForExit = await serveBundle({
entry: 'src/index',
checksEnabled: cmd.check,
+2 -1
View File
@@ -17,10 +17,11 @@
import { ConfigReader } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import { Command } from 'commander';
import { paths } from '../../lib/paths';
import { serveBackend } from '../../lib/bundler/backend';
export default async (cmd: Command) => {
const appConfigs = await loadConfig();
const appConfigs = await loadConfig({ rootPath: paths.targetRoot });
const waitForExit = await serveBackend({
entry: 'src/index',
checksEnabled: cmd.check,
+2 -1
View File
@@ -17,10 +17,11 @@
import { Command } from 'commander';
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { paths } from '../../lib/paths';
import { buildBundle } from '../../lib/bundler';
export default async (cmd: Command) => {
const appConfigs = await loadConfig();
const appConfigs = await loadConfig({ rootPath: paths.targetRoot });
await buildBundle({
entry: 'dev/index',
statsJsonEnabled: cmd.stats,
+2 -1
View File
@@ -17,10 +17,11 @@
import { Command } from 'commander';
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { paths } from '../../lib/paths';
import { serveBundle } from '../../lib/bundler';
export default async (cmd: Command) => {
const appConfigs = await loadConfig();
const appConfigs = await loadConfig({ rootPath: paths.targetRoot });
const waitForExit = await serveBundle({
entry: 'dev/index',
checksEnabled: cmd.check,
+1 -1
View File
@@ -136,7 +136,7 @@ async function findTargetPackages(pkgNames: string[]): Promise<LernaPackage[]> {
// Don't include dependencies of packages that are marked as bundled
if (!node.pkg.get('bundled')) {
const pkgDeps = Object.keys(node.pkg.dependencies);
const pkgDeps = Object.keys(node.pkg.dependencies ?? {});
const localDeps: string[] = Array.from(node.localDependencies.keys());
const filteredDeps = localDeps.filter(dep => pkgDeps.includes(dep));
+2 -131
View File
@@ -14,135 +14,6 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import { dirname, resolve as resolvePath } from 'path';
import { findPaths } from '@backstage/cli-common';
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;
// Monorepo root dir of the cli itself. Only accessible when running inside Backstage repo.
ownRoot: 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 own monorepo root. Only accessible when running inside Backstage repo.
resolveOwnRoot: 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 with a workspace config 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 data = fs.readJsonSync(packagePath);
if (data.workspaces?.packages) {
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) {
return topPath; // We didn't find any root package.json, assume we're not in a monorepo
}
path = newPath;
}
throw new Error(
`Iteration limit reached when searching for root package.json at ${topPath}`,
);
}
// Finds the root of the cli package itself
export function findOwnDir() {
// Known relative locations of package in dist/dev
const pathDist = '..';
const pathDev = '../..';
// Check the closest dir first
const pkgInDist = resolvePath(__dirname, pathDist, 'package.json');
const isDist = fs.pathExistsSync(pkgInDist);
const path = isDist ? pathDist : pathDev;
return resolvePath(__dirname, path);
}
// Finds the root of the monorepo that the cli exists in. Only accessible when running inside Backstage repo.
export function findOwnRootPath(ownDir: string) {
const isLocal = fs.pathExistsSync(resolvePath(ownDir, 'src'));
if (!isLocal) {
throw new Error(
'Tried to access monorepo package root dir outside of Backstage repository',
);
}
return resolvePath(ownDir, '../..');
}
export function findPaths(): Paths {
const ownDir = findOwnDir();
const targetDir = fs.realpathSync(process.cwd());
// Lazy load this as it will throw an error if we're not inside the Backstage repo.
let ownRoot = '';
const getOwnRoot = () => {
if (!ownRoot) {
ownRoot = findOwnRootPath(ownDir);
}
return ownRoot;
};
// 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,
get ownRoot() {
return getOwnRoot();
},
targetDir,
get targetRoot() {
return getTargetRoot();
},
resolveOwn: (...paths) => resolvePath(ownDir, ...paths),
resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths),
resolveTarget: (...paths) => resolvePath(targetDir, ...paths),
resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths),
};
}
export const paths = findPaths();
export const paths = findPaths(__dirname);
-57
View File
@@ -1,57 +0,0 @@
/*
* 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';
/**
* Looks for a package.json that has name: "root" to identify the root of the monorepo
*
* This is a copy of the same function in the CLI
*/
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 data = fs.readJsonSync(packagePath);
if (data.name === 'root' || data.name.includes('backstage-e2e')) {
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}`,
);
}
+3 -11
View File
@@ -14,13 +14,11 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { findRootPath } from './paths';
type ResolveOptions = {
// Same as configPath in LoadConfigOptions
configPath?: string;
// Root path for search for app-config.yaml
rootPath: string;
};
/**
@@ -31,13 +29,7 @@ export async function resolveStaticConfig(
): Promise<string[]> {
// TODO: We'll want this to be a bit more elaborate, probably adding configs for
// specific env, and maybe local config for plugins.
let { configPath } = options;
if (!configPath) {
configPath = resolvePath(
findRootPath(fs.realpathSync(process.cwd())),
'app-config.yaml',
);
}
const configPath = resolvePath(options.rootPath, 'app-config.yaml');
return [configPath];
}
+3 -3
View File
@@ -25,8 +25,8 @@ import {
} from './lib';
export type LoadConfigOptions = {
// Config path, defaults to app-config.yaml in project root
configPath?: string;
// Root path for search for app-config.yaml
rootPath: string;
// Whether to read secrets or omit them, defaults to false.
shouldReadSecrets?: boolean;
@@ -59,7 +59,7 @@ class Context {
}
export async function loadConfig(
options: LoadConfigOptions = {},
options: LoadConfigOptions,
): Promise<AppConfig[]> {
const configs = [];
+1
View File
@@ -25,6 +25,7 @@
"lint": "backstage-cli lint"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.16",
"commander": "^4.1.1",
"chalk": "^4.0.0",
"fs-extra": "^9.0.0",
+4 -2
View File
@@ -21,10 +21,10 @@ import { Command } from 'commander';
import inquirer, { Answers, Question } from 'inquirer';
import { exec as execCb } from 'child_process';
import { resolve as resolvePath } from 'path';
import { findPaths } from '@backstage/cli-common';
import { version } from '../package.json';
import os from 'os';
import { Task, templatingTask } from './lib/tasks';
import { paths } from './lib/paths';
import { version } from './lib/version';
const exec = promisify(execCb);
@@ -88,6 +88,8 @@ async function moveApp(tempDir: string, destination: string, id: string) {
}
export default async (cmd: Command): Promise<void> => {
const paths = findPaths(__dirname);
const questions: Question[] = [
{
type: 'input',
+2 -2
View File
@@ -17,7 +17,7 @@
import program from 'commander';
import chalk from 'chalk';
import { exitWithError } from './lib/errors';
import { version } from './lib/version';
import { version } from '../package.json';
import createApp from './createApp';
const main = (argv: string[]) => {
@@ -29,7 +29,7 @@ const main = (argv: string[]) => {
'--skip-install',
'Skip the install and builds steps after creating the app',
)
.action(createApp)
.action(createApp);
if (!process.argv.slice(2).length) {
program.outputHelp(chalk.yellow);
-150
View File
@@ -1,150 +0,0 @@
/*
* 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;
// Monorepo root dir of the cli itself. Only accessible when running inside Backstage repo.
ownRoot: 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 own monorepo root. Only accessible when running inside Backstage repo.
resolveOwnRoot: 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 data = fs.readJsonSync(packagePath);
if (data.name === 'root' || data.name.includes('backstage-e2e')) {
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}`,
);
}
// Finds the root of the cli package itself
export function findOwnDir() {
// Known relative locations of package in dist/dev
const pathDist = '..';
const pathDev = '../..';
// Check the closest dir first
const pkgInDist = resolvePath(__dirname, pathDist, 'package.json');
const isDist = fs.pathExistsSync(pkgInDist);
const path = isDist ? pathDist : pathDev;
return resolvePath(__dirname, path);
}
// Finds the root of the monorepo that the cli exists in. Only accessible when running inside Backstage repo.
export function findOwnRootPath(ownDir: string) {
const isLocal = fs.pathExistsSync(resolvePath(ownDir, 'src'));
if (!isLocal) {
throw new Error(
'Tried to access monorepo package root dir outside of Backstage repository',
);
}
return resolvePath(ownDir, '../..');
}
export function findPaths(): Paths {
const ownDir = findOwnDir();
const targetDir = fs.realpathSync(process.cwd());
// Lazy load this as it will throw an error if we're not inside the Backstage repo.
let ownRoot = '';
const getOwnRoot = () => {
if (!ownRoot) {
ownRoot = findOwnRootPath(ownDir);
}
return ownRoot;
};
// 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,
get ownRoot() {
return getOwnRoot();
},
targetDir,
get targetRoot() {
return getTargetRoot();
},
resolveOwn: (...paths) => resolvePath(ownDir, ...paths),
resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths),
resolveTarget: (...paths) => resolvePath(targetDir, ...paths),
resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths),
};
}
export const paths = findPaths();
-26
View File
@@ -1,26 +0,0 @@
/*
* 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 { paths } from './paths';
export function findVersion() {
const pkgContent = fs.readFileSync(paths.resolveOwn('package.json'), 'utf8');
return JSON.parse(pkgContent).version;
}
export const version = findVersion();
export const isDev = fs.pathExistsSync(paths.resolveOwn('src'));
@@ -19,8 +19,7 @@
"dependencies": {
"@backstage/backend-common": "^{{version}}",
"@backstage/catalog-model": "^{{version}}",
"@backstage/config": "^0.1.1-alpha.13",
"@backstage/config-loader": "^0.1.1-alpha.13",
"@backstage/config": "^{{version}}",
"@backstage/plugin-auth-backend": "^{{version}}",
"@backstage/plugin-catalog-backend": "^{{version}}",
"@backstage/plugin-identity-backend": "^{{version}}",
@@ -42,7 +41,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.15",
"@backstage/cli": "^{{version}}",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
@@ -24,11 +24,11 @@
import {
createServiceBuilder,
loadBackendConfig,
getRootLogger,
useHotMemoize,
} from '@backstage/backend-common';
import { ConfigReader, AppConfig } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
{{#if dbTypePG}}
import knex, { PgConnectionConfig } from 'knex';
{{/if}}
@@ -78,7 +78,7 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
}
async function main() {
const configs = await loadConfig();
const configs = await loadBackendConfig();
const configReader = ConfigReader.fromConfigs(configs);
const createEnv = makeCreateEnv(configs);
+1 -1
View File
@@ -15,7 +15,7 @@
*/
import { spawn, ChildProcess } from 'child_process';
import program from 'commander';
import { version } from './lib/version';
import { version } from '../package.json';
import path from 'path';
import HTTPServer from './lib/httpServer';
import openBrowser from 'react-dev-utils/openBrowser';
-1
View File
@@ -22,7 +22,6 @@
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.16",
"@backstage/config": "^0.1.1-alpha.16",
"@backstage/config-loader": "^0.1.1-alpha.16",
"@types/express": "^4.17.6",
"body-parser": "^1.19.0",
"compression": "^1.7.4",
@@ -18,9 +18,12 @@ import Knex from 'knex';
import { Server } from 'http';
import { Logger } from 'winston';
import { ConfigReader } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import { createRouter } from './router';
import { createServiceBuilder, useHotMemoize } from '@backstage/backend-common';
import {
createServiceBuilder,
useHotMemoize,
loadBackendConfig,
} from '@backstage/backend-common';
export interface ServerOptions {
logger: Logger;
@@ -30,7 +33,7 @@ export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'auth-backend' });
const config = ConfigReader.fromConfigs(await loadConfig());
const config = ConfigReader.fromConfigs(await loadBackendConfig());
const database = useHotMemoize(module, () => {
const knex = Knex({
-1
View File
@@ -21,7 +21,6 @@
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.16",
"@backstage/config": "^0.1.1-alpha.16",
"@backstage/config-loader": "^0.1.1-alpha.16",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
@@ -17,12 +17,12 @@
import { createRouter } from './router';
import winston from 'winston';
import { ConfigReader } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import { loadBackendConfig } from '@backstage/backend-common';
describe('createRouter', () => {
it('works', async () => {
const logger = winston.createLogger();
const config = ConfigReader.fromConfigs(await loadConfig());
const config = ConfigReader.fromConfigs(await loadBackendConfig());
const router = await createRouter({
config,
logger,
@@ -14,12 +14,14 @@
* limitations under the License.
*/
import { createServiceBuilder } from '@backstage/backend-common';
import {
createServiceBuilder,
loadBackendConfig,
} from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
import { ConfigReader } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
export interface ServerOptions {
port: number;
@@ -34,7 +36,7 @@ export async function startStandaloneServer(
logger.debug('Creating application...');
const config = ConfigReader.fromConfigs(await loadConfig());
const config = ConfigReader.fromConfigs(await loadBackendConfig());
const router = await createRouter({
config,
logger,