refactor(cli): move create-app into his own package
This commit is contained in:
@@ -22,17 +22,6 @@ import { version } from './lib/version';
|
||||
const main = (argv: string[]) => {
|
||||
program.name('backstage-cli').version(version);
|
||||
|
||||
program
|
||||
.command('create-app')
|
||||
.description('Creates a new app in a new directory')
|
||||
.option(
|
||||
'--skip-install',
|
||||
'Skip the install and builds steps after creating the app',
|
||||
)
|
||||
.action(
|
||||
lazyAction(() => import('./commands/create-app/createApp'), 'default'),
|
||||
);
|
||||
|
||||
program
|
||||
.command('app:build')
|
||||
.description('Build an app for a production release')
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
ignorePatterns: ['templates/**'],
|
||||
rules: {
|
||||
'no-console': 0,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
# @backstage/create-app
|
||||
|
||||
This package provides a CLI for creating apps.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the package via npm or yarn:
|
||||
|
||||
```sh
|
||||
$ npm install --save @backstage/create-app
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
$ yarn add @backstage/create-app
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md)
|
||||
- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md)
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
// Figure out whether we're running inside the backstage repo or as an installed dependency
|
||||
const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src'));
|
||||
|
||||
if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) {
|
||||
require('../src');
|
||||
} else {
|
||||
require('ts-node').register({
|
||||
transpileOnly: true,
|
||||
compilerOptions: {
|
||||
module: 'CommonJS',
|
||||
},
|
||||
});
|
||||
|
||||
require('../src');
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@backstage/create-app",
|
||||
"description": "Create app package for Backstage",
|
||||
"version": "0.1.1",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spotify/backstage",
|
||||
"directory": "packages/create-app"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"bin": {
|
||||
"backstage-create-app": "bin/backstage-create-app"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^4.1.1",
|
||||
"chalk": "^4.0.0",
|
||||
"fs-extra": "^9.0.0",
|
||||
"react-dev-utils": "^10.2.1",
|
||||
"inquirer": "^7.0.4",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"ora": "^4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/react-dev-utils": "^9.0.4",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"@types/recursive-readdir": "^2.2.0",
|
||||
"@types/ora": "^3.2.0",
|
||||
"ts-node": "^8.6.2"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"watch": "./src",
|
||||
"ext": "ts"
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -22,9 +22,9 @@ import inquirer, { Answers, Question } from 'inquirer';
|
||||
import { exec as execCb } from 'child_process';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import os from 'os';
|
||||
import { Task, templatingTask } from '../../lib/tasks';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { version } from '../../lib/version';
|
||||
import { Task, templatingTask } from './lib/tasks';
|
||||
import { paths } from './lib/paths';
|
||||
import { version } from './lib/version';
|
||||
|
||||
const exec = promisify(execCb);
|
||||
|
||||
@@ -116,7 +116,7 @@ export default async (cmd: Command): Promise<void> => {
|
||||
answers.dbTypePG = answers.dbType === 'PostgreSQL';
|
||||
answers.dbTypeSqlite = answers.dbType === 'SQLite';
|
||||
|
||||
const templateDir = paths.resolveOwn('templates/default-app');
|
||||
const templateDir = paths.resolveOwnRoot('packages/cli/templates/default-app');
|
||||
const tempDir = resolvePath(os.tmpdir(), answers.name);
|
||||
const appDir = resolvePath(paths.targetDir, answers.name);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 program from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { exitWithError } from './lib/errors';
|
||||
import { version } from './lib/version';
|
||||
|
||||
const main = (argv: string[]) => {
|
||||
program.name('backstage-create-app').version(version);
|
||||
|
||||
program
|
||||
.description('Creates a new app in a new directory')
|
||||
.option(
|
||||
'--skip-install',
|
||||
'Skip the install and builds steps after creating the app',
|
||||
)
|
||||
.action(
|
||||
lazyAction(() => import('./createApp'), 'default'),
|
||||
);
|
||||
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp(chalk.yellow);
|
||||
}
|
||||
|
||||
program.parse(argv);
|
||||
};
|
||||
|
||||
// Wraps an action function so that it always exits and handles errors
|
||||
function lazyAction<T extends readonly any[], Export extends string>(
|
||||
actionRequireFunc: () => Promise<
|
||||
{ [name in Export]: (...args: T) => Promise<any> }
|
||||
>,
|
||||
exportName: Export,
|
||||
): (...args: T) => Promise<never> {
|
||||
return async (...args: T) => {
|
||||
try {
|
||||
const module = await actionRequireFunc();
|
||||
const actionFunc = module[exportName];
|
||||
await actionFunc(...args);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
exitWithError(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', rejection => {
|
||||
if (rejection instanceof Error) {
|
||||
exitWithError(rejection);
|
||||
} else {
|
||||
exitWithError(new Error(`Unknown rejection: '${rejection}'`));
|
||||
}
|
||||
});
|
||||
|
||||
main(process.argv);
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 chalk from 'chalk';
|
||||
|
||||
export class CustomError extends Error {
|
||||
get name(): string {
|
||||
return this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExitCodeError extends CustomError {
|
||||
readonly code: number;
|
||||
|
||||
constructor(code: number, command?: string) {
|
||||
if (command) {
|
||||
super(`Command '${command}' exited with code ${code}`);
|
||||
} else {
|
||||
super(`Child exited with code ${code}`);
|
||||
}
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export function exitWithError(error: Error): never {
|
||||
if (error instanceof ExitCodeError) {
|
||||
process.stderr.write(`\n${chalk.red(error.message)}\n\n`);
|
||||
process.exit(error.code);
|
||||
} else {
|
||||
process.stderr.write(`\n${chalk.red(`${error}`)}\n\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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();
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import handlebars from 'handlebars';
|
||||
import ora from 'ora';
|
||||
import { basename, dirname } from 'path';
|
||||
import recursive from 'recursive-readdir';
|
||||
|
||||
const TASK_NAME_MAX_LENGTH = 14;
|
||||
|
||||
export class Task {
|
||||
static log(name: string = '') {
|
||||
process.stdout.write(`${chalk.green(name)}\n`);
|
||||
}
|
||||
|
||||
static error(message: string = '') {
|
||||
process.stdout.write(`\n${chalk.red(message)}\n\n`);
|
||||
}
|
||||
|
||||
static section(name: string) {
|
||||
const title = chalk.green(`${name}:`);
|
||||
process.stdout.write(`\n ${title}\n`);
|
||||
}
|
||||
|
||||
static exit(code: number = 0) {
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
static async forItem(
|
||||
task: string,
|
||||
item: string,
|
||||
taskFunc: () => Promise<void>,
|
||||
): Promise<void> {
|
||||
const paddedTask = chalk.green(task.padEnd(TASK_NAME_MAX_LENGTH));
|
||||
|
||||
const spinner = ora({
|
||||
prefixText: chalk.green(` ${paddedTask}${chalk.cyan(item)}`),
|
||||
spinner: 'arc',
|
||||
color: 'green',
|
||||
}).start();
|
||||
|
||||
try {
|
||||
await taskFunc();
|
||||
spinner.succeed();
|
||||
} catch (error) {
|
||||
spinner.fail();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function templatingTask(
|
||||
templateDir: string,
|
||||
destinationDir: string,
|
||||
context: any,
|
||||
) {
|
||||
const files = await recursive(templateDir).catch(error => {
|
||||
throw new Error(`Failed to read template directory: ${error.message}`);
|
||||
});
|
||||
|
||||
for (const file of files) {
|
||||
const destinationFile = file.replace(templateDir, destinationDir);
|
||||
await fs.ensureDir(dirname(destinationFile));
|
||||
|
||||
if (file.endsWith('.hbs')) {
|
||||
await Task.forItem('templating', basename(file), async () => {
|
||||
const destination = destinationFile.replace(/\.hbs$/, '');
|
||||
|
||||
const template = await fs.readFile(file);
|
||||
const compiled = handlebars.compile(template.toString());
|
||||
const contents = compiled({ name: basename(destination), ...context });
|
||||
|
||||
await fs.writeFile(destination, contents).catch(error => {
|
||||
throw new Error(
|
||||
`Failed to create file: ${destination}: ${error.message}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await Task.forItem('copying', basename(file), async () => {
|
||||
await fs.copyFile(file, destinationFile).catch(error => {
|
||||
const destination = destinationFile;
|
||||
throw new Error(
|
||||
`Failed to copy file to ${destination} : ${error.message}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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'));
|
||||
Reference in New Issue
Block a user