cli: create command factory foundations

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-10-17 16:59:59 +02:00
parent 8acb23b29f
commit e607456dbc
5 changed files with 182 additions and 4 deletions
@@ -0,0 +1,76 @@
/*
* Copyright 2021 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 inquirer from 'inquirer';
import { AnyFactory } from './types';
import * as factories from './factories';
import partition from 'lodash/partition';
export class FactoryRegistry {
private static factoryMap = new Map<string, AnyFactory>(
Object.values(factories).map(factory => [factory.name, factory]),
);
static async interactiveSelect(preselected?: string): Promise<AnyFactory> {
let selected = preselected;
if (!selected) {
const answers = await inquirer.prompt<{ name: string }>([
{
type: 'list',
name: 'name',
message: 'What do you want to create?',
choices: Array.from(this.factoryMap.values()).map(factory => ({
name: `${factory.name} - ${factory.description}`,
value: factory.name,
})),
},
]);
selected = answers.name;
}
const factory = this.factoryMap.get(selected);
if (!factory) {
throw new Error(`Unknown selection '${selected}'`);
}
return factory;
}
static async populateOptions(
factory: AnyFactory,
provided: Record<string, string>,
): Promise<Record<string, string>> {
const [hasAnswers, needsAnswers] = partition(
factory.options,
option => option.name in provided,
);
for (const option of hasAnswers) {
const value = provided[option.name];
if (option.validate) {
const result = option.validate(value);
if (result !== true) {
throw new Error(`Invalid option '${option.name}'. ${result}`);
}
}
}
const answers = await inquirer.prompt(needsAnswers);
return { ...provided, ...answers };
}
}
+9 -4
View File
@@ -15,6 +15,7 @@
*/
import { Command } from 'commander';
import { FactoryRegistry } from './FactoryRegistry';
function parseOptions(optionStrings: string[]): Record<string, string> {
const options: Record<string, string> = {};
@@ -34,9 +35,13 @@ function parseOptions(optionStrings: string[]): Record<string, string> {
}
export default async (cmd: Command) => {
const selected = cmd.opts().select;
console.log('DEBUG: selected =', selected);
const factory = await FactoryRegistry.interactiveSelect(cmd.opts().select);
const options = parseOptions(cmd.opts().option);
console.log('DEBUG: options =', options);
const providedOptions = parseOptions(cmd.opts().option);
const options = await FactoryRegistry.populateOptions(
factory,
providedOptions,
);
await factory.create(options);
};
@@ -0,0 +1,46 @@
/*
* Copyright 2021 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 { createFactory } from '../types';
type Options = {
id: string;
};
export const frontendPlugin = createFactory<Options>({
name: 'plugin',
description: 'A new frontend plugin',
options: [
{
type: 'input',
name: 'id',
message: 'Enter an ID for the plugin',
validate: (value: string) => {
if (!value) {
return 'Please enter an ID for the plugin';
} else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {
return 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.';
}
return true;
},
},
],
async create(options: Options) {
console.log(
`Creating ${this.name} with options ${JSON.stringify(options)}`,
);
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2021 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.
*/
export { frontendPlugin } from './frontendPlugin';
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright 2021 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 { DistinctQuestion } from 'inquirer';
export type AnyOptions = Record<string, string>;
export interface Factory<Options extends AnyOptions> {
name: string;
description: string;
options: ReadonlyArray<DistinctQuestion<Options> & { name: string }>;
create(options: Options): Promise<void>;
}
export type AnyFactory = Factory<AnyOptions>;
export function createFactory<Options extends AnyOptions>(
config: Factory<Options>,
): AnyFactory {
return config as AnyFactory;
}