Reorganize code

Signed-off-by: Min Kim <minkimcello@gmail.com>
This commit is contained in:
Min Kim
2024-12-10 19:39:16 -05:00
parent d71e11faa2
commit 4bfe2110fd
5 changed files with 317 additions and 223 deletions
+77 -219
View File
@@ -17,164 +17,20 @@
import os from 'os';
import fs from 'fs-extra';
import { join as joinPath, dirname } from 'path';
import { OptionValues } from 'commander';
import inquirer from 'inquirer';
import { parse } from 'yaml';
import { FactoryRegistry } from '../../lib/new/FactoryRegistry';
import { isMonoRepo } from '@backstage/cli-node';
import { paths } from '../../lib/paths';
import { assertError } from '@backstage/errors';
import { Task } from '../../lib/tasks';
import { executePluginPackageTemplate } from '../../lib/new/factories/common/tasks';
import {
pluginIdPrompt,
moduleIdIdPrompt,
// ownerPrompt, // 🚨 WIP
} from '../../lib/new/factories/common/prompts';
import defaultTemplates from '../../../templates';
type ConfigurablePrompt =
| {
id: string;
prompt: string;
type?: string;
validate?: string;
default?: string | boolean;
}
| string;
interface Template {
description?: string;
template: string;
targetPath: string;
prompts?: ConfigurablePrompt[];
additionalActions?: string[];
}
interface TemplateLocation {
id: string;
target: string;
}
async function readCliConfig(
cliConfig:
| {
defaults?: boolean;
templates?: TemplateLocation[];
globals?: Record<string, string>;
}
| undefined,
) {
let templates: TemplateLocation[] = [];
const cliTemplates = cliConfig?.templates;
if (!cliConfig || cliConfig?.defaults) {
templates = defaultTemplates;
}
if (cliTemplates?.length) {
cliTemplates.forEach((template: TemplateLocation) => {
templates.push({
id: template.id,
target: template.target,
});
});
}
return {
templates,
globals: { ...cliConfig?.globals },
};
}
async function templateSelector(
templates: TemplateLocation[],
): Promise<TemplateLocation> {
const answer = await inquirer.prompt<{ name: TemplateLocation }>([
{
type: 'list',
name: 'name',
message: 'What do you want to create?',
choices: templates.map(template => {
return {
name: template.id,
value: template,
};
}),
},
]);
return answer.name;
}
async function verifyTemplate({ target }: TemplateLocation): Promise<Template> {
if (target.startsWith('http')) {
throw new Error('🚨 WIP');
}
const template = parse(fs.readFileSync(target, 'utf-8'));
const templatePath = paths.resolveTargetRoot(
dirname(target),
template.template,
);
if (!fs.existsSync(templatePath)) {
throw new Error(
`Your CLI template skeleton does not exist: ${templatePath}`,
);
}
return template;
}
async function promptOptions({
prompts,
globals,
}: {
prompts: ConfigurablePrompt[];
globals: Record<string, string>;
}): Promise<Record<string, string>> {
const answers = await inquirer.prompt(
prompts.map((prompt: ConfigurablePrompt) => {
if (typeof prompt === 'string') {
switch (prompt) {
case 'id':
return pluginIdPrompt();
case 'moduleid':
return moduleIdIdPrompt();
default:
throw new Error(
`There is no built-in prompt with the following id: ${prompt}`,
);
}
}
return {
type: prompt.type || 'input',
name: prompt.id,
message: prompt.prompt,
default:
globals[prompt.id] !== undefined
? globals[prompt.id]
: prompt.default,
validate: (value: string) => {
if (!value) {
return `Please provide a value for ${prompt.id}`;
} else if (prompt.validate) {
let valid: boolean;
let message: string;
switch (prompt.validate) {
case 'backstage-id':
valid = /^[a-z0-9]+(-[a-z0-9]+)*$/.test(value);
message =
'Value must be lowercase and contain only letters, digits, and dashes.';
break;
default:
throw new Error(
`There is no built-in validator with the following id: ${prompt.validate}`,
);
}
return valid || message;
}
return true;
},
};
}),
);
return { ...globals, ...answers };
}
readCliConfig,
templateSelector,
verifyTemplate,
promptOptions,
populateOptions,
} from './util';
export default async () => {
const pkgJson = await fs.readJson(paths.resolveTargetRoot('package.json'));
@@ -182,78 +38,80 @@ export default async () => {
const { templates, globals } = await readCliConfig(cliConfig);
const template = await verifyTemplate(await templateSelector(templates));
console.log(
await promptOptions({
prompts: template.prompts || [],
globals,
}),
);
// let defaultVersion = '0.1.0';
// 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;
// }
// }
const prompts = await promptOptions({
prompts: template.prompts || [],
globals,
});
const options = await populateOptions(prompts, template);
// const tempDirs = new Array<string>();
// async function createTemporaryDirectory(name: string): Promise<string> {
// const dir = await fs.mkdtemp(joinPath(os.tmpdir(), name));
// tempDirs.push(dir);
// return dir;
// }
const tempDirs = new Array<string>();
async function createTemporaryDirectory(name: string): Promise<string> {
const dir = await fs.mkdtemp(joinPath(os.tmpdir(), name));
tempDirs.push(dir);
return dir;
}
// const license = opts.license ?? 'Apache-2.0';
let modified = false;
try {
await executePluginPackageTemplate(
{
private: options.private,
defaultVersion: options.baseVersion,
license: options.license,
isMonoRepo: await isMonoRepo(),
createTemporaryDirectory,
markAsModified() {
modified = true;
},
},
{
targetDir: options.targetDir,
templateDir: template.templatePath,
values: {
name: options.id,
pluginVersion: options.baseVersion,
...options,
},
},
);
// let modified = false;
// try {
// await factory.create(options, {
// isMonoRepo: await isMonoRepo(),
// defaultVersion,
// license,
// scope: opts.scope?.replace(/^@/, ''),
// npmRegistry: opts.npmRegistry,
// private: Boolean(opts.private),
// createTemporaryDirectory,
// markAsModified() {
// modified = true;
// },
// });
// create scope prompt
// npmregistry prompt
// incorporate owners prompt
// additional actions
// add to frontend
// add to backend
// install and lint
// Task.log();
// Task.log(`🎉 Successfully created ${factory.name}`);
// Task.log();
// } catch (error) {
// assertError(error);
// Task.error(error.message);
Task.log();
Task.log(`🎉 Successfully created ${template.id}`);
Task.log();
} catch (error) {
assertError(error);
Task.error(error.message);
// if (modified) {
// Task.log('It seems that something went wrong in the creation process 🤔');
// Task.log();
// Task.log(
// 'We have left the changes that were made intact in case you want to',
// );
// Task.log(
// 'continue manually, but you can also revert the changes and try again.',
// );
if (modified) {
Task.log('It seems that something went wrong in the creation process 🤔');
Task.log();
Task.log(
'We have left the changes that were made intact in case you want to',
);
Task.log(
'continue manually, but you can also revert the changes and try again.',
);
// Task.error(`🔥 Failed to create ${factory.name}!`);
// }
// } finally {
// for (const dir of tempDirs) {
// try {
// await fs.remove(dir);
// } catch (error) {
// console.error(
// `Failed to remove temporary directory '${dir}', ${error}`,
// );
// }
// }
// }
Task.error(`🔥 Failed to create ${template.id}!`);
}
} finally {
for (const dir of tempDirs) {
try {
await fs.remove(dir);
} catch (error) {
console.error(
`Failed to remove temporary directory '${dir}', ${error}`,
);
}
}
}
};
+38
View File
@@ -0,0 +1,38 @@
/*
* Copyright 2024 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 type ConfigurablePrompt =
| {
id: string;
prompt: string;
validate?: string;
default?: string | boolean;
}
| string;
export interface Template {
id: string;
description?: string;
template: string;
templatePath: string;
targetPath: string;
prompts?: ConfigurablePrompt[];
additionalActions?: string[];
}
export interface TemplateLocation {
id: string;
target: string;
}
+201
View File
@@ -0,0 +1,201 @@
/*
* Copyright 2024 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 { dirname } from 'path';
import { parse } from 'yaml';
import fs from 'fs-extra';
import { paths } from '../../lib/paths';
import {
pluginIdPrompt,
moduleIdIdPrompt,
npmRegistryPrompt,
ownerPrompt,
} from '../../lib/new/factories/common/prompts';
import defaultTemplates from '../../../templates';
import { Template, TemplateLocation, ConfigurablePrompt } from './types';
export async function readCliConfig(
cliConfig:
| {
defaults?: boolean;
templates?: TemplateLocation[];
globals?: Record<string, string>;
}
| undefined,
) {
let templates: TemplateLocation[] = [];
if (!cliConfig || cliConfig?.defaults) {
templates = defaultTemplates;
}
const cliTemplates = cliConfig?.templates;
if (cliTemplates?.length) {
cliTemplates.forEach((template: TemplateLocation) => {
templates.push({
id: template.id,
target: template.target,
});
});
}
return {
templates,
globals: { ...cliConfig?.globals },
};
}
export async function templateSelector(
templates: TemplateLocation[],
): Promise<TemplateLocation> {
const answer = await inquirer.prompt<{ name: TemplateLocation }>([
{
type: 'list',
name: 'name',
message: 'What do you want to create?',
choices: templates.map(template => {
return {
name: template.id,
value: template,
};
}),
},
]);
return answer.name;
}
export async function verifyTemplate({
id,
target,
}: TemplateLocation): Promise<Template> {
if (target.startsWith('http')) {
throw new Error('Remote templates are not supported yet');
}
const template = parse(fs.readFileSync(target, 'utf-8'));
const templatePath = paths.resolveTargetRoot(
dirname(target),
template.template,
);
if (!fs.existsSync(templatePath)) {
throw new Error(
`Your CLI template skeleton does not exist: ${templatePath}`,
);
}
if (!template.targetPath) {
throw new Error(`Your template, ${id}, is missing a targetPath`);
}
return { id, templatePath, ...template };
}
export async function promptOptions({
prompts,
globals,
}: {
prompts: ConfigurablePrompt[];
globals: Record<string, string | boolean>;
}): Promise<Record<string, string | boolean>> {
const answers = await inquirer.prompt(
prompts.map((prompt: ConfigurablePrompt) => {
if (typeof prompt === 'string') {
switch (prompt) {
case 'id':
return pluginIdPrompt();
case 'moduleid':
return moduleIdIdPrompt();
case 'npmregistry':
return npmRegistryPrompt();
case 'owner':
return ownerPrompt();
default:
throw new Error(
`There is no built-in prompt with the following id: ${prompt}`,
);
}
}
return {
type: 'input',
name: prompt.id,
message: prompt.prompt,
default:
globals[prompt.id] !== undefined
? globals[prompt.id]
: prompt.default,
validate: (value: string) => {
if (!value) {
return `Please provide a value for ${prompt.id}`;
} else if (prompt.validate) {
let valid: boolean;
let message: string;
switch (prompt.validate) {
case 'backstage-id':
valid = /^[a-z0-9]+(-[a-z0-9]+)*$/.test(value);
message =
'Value must be lowercase and contain only letters, digits, and dashes.';
break;
default:
throw new Error(
`There is no built-in validator with the following id: ${prompt.validate}`,
);
}
return valid || message;
}
return true;
},
};
}),
);
return { ...globals, ...answers };
}
interface Options extends Record<string, string | boolean> {
id: string;
private: boolean;
baseVersion: string;
license: string;
targetDir: string;
}
async function calculateBaseVersion(baseVersion: string) {
if (!baseVersion) {
const lernaVersion = await fs
.readJson(paths.resolveTargetRoot('lerna.json'))
.then(pkg => pkg.version)
.catch(() => undefined);
if (lernaVersion) {
return lernaVersion;
}
return '0.1.0';
}
return baseVersion;
}
export async function populateOptions(
prompts: Record<string, string | boolean>,
template: Template,
): Promise<Options> {
return {
id: prompts.id as string,
private: (prompts.private as boolean) ?? false,
baseVersion: await calculateBaseVersion(prompts.baseVersion as string),
license: (prompts.license as string) ?? 'Apache-2.0',
targetDir: paths.resolveTargetRoot(
template.targetPath,
prompts.id as string,
),
...prompts,
};
}
+1 -1
View File
@@ -16,7 +16,7 @@ export default [
target: '@backstage/cli/templates/alpha/default-node-plugin-package.yaml',
},
{
id: 'frontend-plugion', // changed from 'plugin'
id: 'frontend-plugin', // changed from 'plugin'
target: '@backstage/cli/templates/alpha/default-plugin.yaml',
},
{
@@ -8,6 +8,3 @@ prompts:
prompt: Provide another id
default: another-id-example
validate: backstage-id
- id: private
prompt: Would you like for this package to be private?
type: confirm