Toolkit: Remove plugin:create and component:create commands (#66729)

This commit is contained in:
Esteban Beltran
2023-04-19 15:15:37 +02:00
committed by GitHub
parent 46b73548b3
commit 5dac0c9871
15 changed files with 4 additions and 662 deletions

View File

@ -1,12 +1,10 @@
import chalk from 'chalk';
import { program } from 'commander';
import { componentCreateTask } from './tasks/component.create';
import { nodeVersionCheckerTask } from './tasks/nodeVersionChecker';
import { buildPackageTask } from './tasks/package.build';
import { pluginBuildTask } from './tasks/plugin.build';
import { ciBuildPluginTask, ciPackagePluginTask, ciPluginReportTask } from './tasks/plugin.ci';
import { pluginCreateTask } from './tasks/plugin.create';
import { pluginDevTask } from './tasks/plugin.dev';
import { pluginSignTask } from './tasks/plugin.sign';
import { pluginTestTask } from './tasks/plugin.tests';
@ -82,24 +80,6 @@ export const run = (includeInternalScripts = false) => {
);
await execTask(searchTestDataSetupTask)({ count: cmd.count });
});
// React generator
program
.command('component:create')
.description(
'[deprecated] Scaffold React components. Optionally add test, story and .mdx files. The components are created in the same dir the script is run from.'
)
.action(async () => {
console.log(
chalk.yellow.bold(
`⚠️ This command is deprecated and will be removed in v10. No further support will be provided. ⚠️`
)
);
console.log(
'if you were reliant on this command we recommend https://www.npmjs.com/package/react-gen-component'
);
await execTask(componentCreateTask)({});
});
}
program.option('-v, --version', 'Toolkit version').action(async () => {
@ -109,13 +89,12 @@ export const run = (includeInternalScripts = false) => {
program
.command('plugin:create [name]')
.description('[Deprecated] Creates plugin from template')
.action(async (cmd) => {
console.log(chalk.yellow('\n⚠ DEPRECATED. This command is deprecated and will be removed in v10. ⚠️'));
.description('[removed] Use grafana create-plugin instead')
.action(async () => {
console.log(
'Please migrate to grafana create-plugin https://github.com/grafana/plugin-tools/tree/main/packages/create-plugin\n'
'No longer supported. Use grafana create-plugin https://github.com/grafana/plugin-tools/tree/main/packages/create-plugin\n'
);
await execTask(pluginCreateTask)({ name: cmd, silent: true });
process.exit(1);
});
program

View File

@ -1,88 +0,0 @@
import fs from 'fs';
import { prompt } from 'inquirer';
import { template as _template } from 'lodash';
import { componentTpl, docsTpl, storyTpl, testTpl } from '../templates';
import { pascalCase } from '../utils/pascalCase';
import { promptConfirm, promptInput, promptList } from '../utils/prompt';
import { Task, TaskRunner } from './task';
interface Details {
name?: string;
hasStory: boolean;
group?: string;
isStoryPublic: boolean;
hasTests: boolean;
}
interface GeneratorOptions {
details: Details;
path: string;
}
type ComponentGenerator = (options: GeneratorOptions) => Promise<any>;
const componentGroups = [
{ name: 'General', value: 'General' },
{ name: 'Forms', value: 'Forms' },
{ name: 'Panel', value: 'Panel' },
{ name: 'Visualizations', value: 'Visualizations' },
{ name: 'Others', value: 'Others' },
];
export const promptDetails = () => {
return prompt<Details>([
promptInput('name', 'Component name', true),
promptConfirm('hasTests', "Generate component's test file?"),
promptConfirm('hasStory', "Generate component's story file?"),
promptConfirm(
'isStoryPublic',
'Generate public story? (Selecting "No" will create an internal story)',
true,
({ hasStory }) => hasStory
),
promptList(
'group',
'Select component group for the story (e.g. Forms, Layout)',
() => componentGroups,
0,
({ hasStory }) => hasStory
),
]);
};
export const generateComponents: ComponentGenerator = async ({ details, path }) => {
const name = pascalCase(details.name);
const getCompiled = (template: string) => {
return _template(template)({ ...details, name });
};
const filePath = `${path}/${name}`;
let paths = [];
fs.writeFileSync(`${filePath}.tsx`, getCompiled(componentTpl));
paths.push(`${filePath}.tsx`);
if (details.hasTests) {
fs.writeFileSync(`${filePath}.test.tsx`, getCompiled(testTpl));
paths.push(`${filePath}.test.tsx`);
}
if (details.hasStory) {
const storyExt = details.isStoryPublic ? '.story.tsx' : '.story.internal.tsx';
fs.writeFileSync(`${filePath}${storyExt}`, getCompiled(storyTpl));
fs.writeFileSync(`${filePath}.mdx`, getCompiled(docsTpl));
paths.push(`${filePath}${storyExt}`, `${filePath}.mdx`);
}
console.log('Generated files:');
console.log(paths.join('\n'));
};
const componentCreateRunner: TaskRunner<any> = async () => {
const destPath = process.cwd();
const details = await promptDetails();
await generateComponents({ details, path: destPath });
};
export const componentCreateTask = new Task('component:create', componentCreateRunner);

View File

@ -1,57 +0,0 @@
import { prompt } from 'inquirer';
import path from 'path';
import { promptConfirm } from '../utils/prompt';
import {
fetchTemplate,
formatPluginDetails,
getPluginIdFromName,
prepareJsonFiles,
printGrafanaTutorialsDetails,
promptPluginDetails,
promptPluginType,
removeGitFiles,
verifyGitExists,
removeLockFile,
} from './plugin/create';
import { Task, TaskRunner } from './task';
interface PluginCreateOptions {
name?: string;
}
const pluginCreateRunner: TaskRunner<PluginCreateOptions> = async ({ name }) => {
const destPath = path.resolve(process.cwd(), getPluginIdFromName(name || ''));
let pluginDetails;
// 1. Verifying if git exists in user's env as templates are cloned from git templates
await verifyGitExists();
// 2. Prompt plugin template
const { type } = await promptPluginType();
// 3. Fetch plugin template from GitHub
await fetchTemplate({ type, dest: destPath });
// 4. Prompt plugin details
do {
pluginDetails = await promptPluginDetails(name);
formatPluginDetails(pluginDetails);
} while ((await prompt<{ confirm: boolean }>(promptConfirm('confirm', 'Is that ok?'))).confirm === false);
// 5. Update json files (package.json, src/plugin.json)
await prepareJsonFiles({ type: type, pluginDetails, pluginPath: destPath });
// 6. Starter templates include `yarn.lock` files which will rarely (if ever) be in sync with `latest` dist-tag
// so best to remove it after cloning.
removeLockFile({ pluginPath: destPath });
// 7. Remove cloned repository .git dir
await removeGitFiles(destPath);
// 8. Promote Grafana Tutorials :)
printGrafanaTutorialsDetails(type);
};
export const pluginCreateTask = new Task<PluginCreateOptions>('plugin:create task', pluginCreateRunner);

View File

@ -1,199 +0,0 @@
import chalk from 'chalk';
import commandExists from 'command-exists';
import { promises as fs, readFileSync, existsSync, unlinkSync } from 'fs';
import { prompt } from 'inquirer';
import { kebabCase } from 'lodash';
import path from 'path';
import gitPromise from 'simple-git';
import { promptConfirm, promptInput } from '../../utils/prompt';
import { rmdir } from '../../utils/rmdir';
import { useSpinner } from '../../utils/useSpinner';
const simpleGit = gitPromise(process.cwd());
interface PluginDetails {
name: string;
org: string;
description: string;
author: boolean | string;
url: string;
keywords: string;
}
type PluginType = 'panel-plugin' | 'datasource-plugin' | 'backend-datasource-plugin';
const PluginNames: Record<PluginType, string> = {
'panel-plugin': 'Grafana Panel Plugin',
'datasource-plugin': 'Grafana Data Source Plugin',
'backend-datasource-plugin': 'Grafana Backend Datasource Plugin',
};
const RepositoriesPaths: Record<PluginType, string> = {
'panel-plugin': 'https://github.com/grafana/simple-react-panel.git',
'datasource-plugin': 'https://github.com/grafana/simple-datasource.git',
'backend-datasource-plugin': 'https://github.com/grafana/simple-datasource-backend.git',
};
const TutorialPaths: Record<PluginType, string> = {
'panel-plugin': 'https://grafana.com/tutorials/build-a-panel-plugin',
'datasource-plugin': 'https://grafana.com/tutorials/build-a-data-source-plugin',
'backend-datasource-plugin': 'TODO',
};
export const getGitUsername = async () => {
const name = await simpleGit.raw(['config', '--global', 'user.name']);
return name || '';
};
export const getPluginIdFromName = (name: string) => kebabCase(name);
export const getPluginId = (pluginDetails: PluginDetails) =>
`${kebabCase(pluginDetails.org)}-${getPluginIdFromName(pluginDetails.name)}`;
export const getPluginKeywords = (pluginDetails: PluginDetails) =>
pluginDetails.keywords
.split(',')
.map((k) => k.trim())
.filter((k) => k !== '');
export const verifyGitExists = async () => {
return new Promise((resolve, reject) => {
commandExists('git', (err, exists) => {
if (exists) {
resolve(true);
}
reject(new Error('git is not installed'));
});
});
};
export const promptPluginType = async () =>
prompt<{ type: PluginType }>([
{
type: 'list',
message: 'Select plugin type',
name: 'type',
choices: [
{ name: 'Panel Plugin', value: 'panel-plugin' },
{ name: 'Datasource Plugin', value: 'datasource-plugin' },
{ name: 'Backend Datasource Plugin', value: 'backend-datasource-plugin' },
],
},
]);
export const promptPluginDetails = async (name?: string) => {
const username = (await getGitUsername()).trim();
const responses = await prompt<PluginDetails>([
promptInput('name', 'Plugin name', true, name),
promptInput('org', 'Organization (used as part of plugin ID)', true),
promptInput('description', 'Description'),
promptInput('keywords', 'Keywords (separated by comma)'),
// Try using git specified username
promptConfirm('author', `Author (${username})`, username, username !== ''),
// Prompt for manual author entry if no git user.name specified
promptInput('author', `Author`, true, undefined, (answers: any) => !answers.author || username === ''),
promptInput('url', 'Your URL (i.e. organisation url)'),
]);
return {
...responses,
author: responses.author === true ? username : responses.author,
};
};
export const fetchTemplate = ({ type, dest }: { type: PluginType; dest: string }) =>
useSpinner('Fetching plugin template...', async () => {
const url = RepositoriesPaths[type];
if (!url) {
throw new Error('Unknown plugin type');
}
await simpleGit.clone(url, dest);
});
export const prepareJsonFiles = ({
type,
pluginDetails,
pluginPath,
}: {
type: PluginType;
pluginDetails: PluginDetails;
pluginPath: string;
}) =>
useSpinner('Saving package.json and plugin.json files', async () => {
const packageJsonPath = path.resolve(pluginPath, 'package.json');
const pluginJsonPath = path.resolve(pluginPath, 'src/plugin.json');
const packageJson: any = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
const pluginJson: any = JSON.parse(readFileSync(pluginJsonPath, 'utf8'));
const pluginId = `${kebabCase(pluginDetails.org)}-${getPluginIdFromName(pluginDetails.name)}`;
packageJson.name = pluginId;
packageJson.author = pluginDetails.author;
packageJson.description = pluginDetails.description;
pluginJson.name = pluginDetails.name;
pluginJson.id = pluginId;
if (type === 'backend-datasource-plugin') {
pluginJson.backend = true;
pluginJson.executable = 'gpx_' + pluginDetails.name;
}
pluginJson.info = {
...pluginJson.info,
description: pluginDetails.description,
author: {
name: pluginDetails.author,
url: pluginDetails.url,
},
keywords: getPluginKeywords(pluginDetails),
};
await Promise.all(
[packageJson, pluginJson].map((f, i) => {
const filePath = i === 0 ? packageJsonPath : pluginJsonPath;
return fs.writeFile(filePath, JSON.stringify(f, null, 2));
})
);
});
export const removeGitFiles = (pluginPath: string) =>
useSpinner('Cleaning', async () => rmdir(`${path.resolve(pluginPath, '.git')}`));
/* eslint-disable no-console */
export const formatPluginDetails = (details: PluginDetails) => {
console.group();
console.log();
console.log(chalk.bold.yellow('Your plugin details'));
console.log('---');
console.log(chalk.bold('Name: '), details.name);
console.log(chalk.bold('ID: '), getPluginId(details));
console.log(chalk.bold('Description: '), details.description);
console.log(chalk.bold('Keywords: '), getPluginKeywords(details));
console.log(chalk.bold('Author: '), details.author);
console.log(chalk.bold('Organisation: '), details.org);
console.log(chalk.bold('Website: '), details.url);
console.log();
console.groupEnd();
};
export const printGrafanaTutorialsDetails = (type: PluginType) => {
console.group();
console.log();
console.log(chalk.bold.yellow(`Congrats! You have just created ${PluginNames[type]}.`));
console.log('Please run `yarn install` to install frontend dependencies.');
console.log();
if (type !== 'backend-datasource-plugin') {
console.log(`${PluginNames[type]} tutorial: ${TutorialPaths[type]}`);
}
console.log(
'Learn more about Grafana Plugins at https://grafana.com/docs/grafana/latest/plugins/developing/development/'
);
console.log();
console.groupEnd();
};
/* eslint-enable no-console */
export const removeLockFile = ({ pluginPath }: { pluginPath: string }) => {
const lockFilePath = path.resolve(pluginPath, 'yarn.lock');
if (existsSync(lockFilePath)) {
unlinkSync(lockFilePath);
}
};

View File

@ -1,12 +0,0 @@
export const testTpl = `
import React from 'react';
import { render, screen } from '@testing-library/react';
import { <%= name %> } from './<%= name %>';
describe('<%= name %>', () => {
it.skip('should render', () => {
});
});
`;

View File

@ -1,10 +0,0 @@
export const componentTpl = `import React from 'react';
export interface Props {};
export const <%= name %> = (props: Props) => {
return (
<div>Hello world!</div>
)
};
`;

View File

@ -1,16 +0,0 @@
export const docsTpl = `import { ArgsTable } from '@storybook/addon-docs/blocks';
import { <%= name %> } from './<%= name %>';
# <%= name %>
### Usage
\`\`\`jsx
import { <%= name %> } from '@grafana/ui';
<<%= name %> />
\`\`\`
### Props
<ArgsTable of={<%= name %>} />
`;

View File

@ -1,4 +0,0 @@
export { componentTpl } from './component.tsx.template';
export { storyTpl } from './story.tsx.template';
export { docsTpl } from './docs.mdx.template';
export { testTpl } from './component.test.tsx.template';

View File

@ -1,22 +0,0 @@
export const storyTpl = `
import React from 'react';
import { <%= name %> } from './<%= name %>';
import { withCenteredStory } from '@grafana/ui/src/utils/storybook/withCenteredStory';
import mdx from './<%= name %>.mdx';
export default {
title: '<%= group %>/<%= name %>',
component: <%= name %>,
decorators: [withCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
export const Basic = () => {
return <<%= name %> />;
};
`;

View File

@ -1,3 +0,0 @@
import { flow, camelCase, upperFirst } from 'lodash';
export const pascalCase = flow([camelCase, upperFirst]);

View File

@ -1,79 +0,0 @@
import inquirer, {
Question,
InputQuestion,
CheckboxQuestion,
NumberQuestion,
PasswordQuestion,
EditorQuestion,
ConfirmQuestion,
ListQuestion,
ChoiceOptions,
} from 'inquirer';
type QuestionWithValidation<A extends inquirer.Answers = inquirer.Answers> =
| InputQuestion<A>
| CheckboxQuestion<A>
| NumberQuestion<A>
| PasswordQuestion<A>
| EditorQuestion<A>;
export const answerRequired = <A extends inquirer.Answers>(question: QuestionWithValidation<A>): Question<A> => {
return {
...question,
validate: (answer: A) => answer.trim() !== '' || `${question.name} is required`,
};
};
export const promptInput = <A extends inquirer.Answers>(
name: string,
message: string | ((answers: A) => string),
required = false,
def: any = undefined,
when: boolean | ((answers: A) => boolean | Promise<boolean>) = true
) => {
const model: InputQuestion<A> = {
type: 'input',
name,
message,
default: def,
when,
};
return required ? answerRequired(model) : model;
};
export const promptList = <A extends inquirer.Answers>(
name: string,
message: string | ((answers: A) => string),
choices: () => ChoiceOptions[],
def: any = undefined,
when: boolean | ((answers: A) => boolean | Promise<boolean>) = true
) => {
const model: ListQuestion<A> = {
type: 'list',
name,
message,
choices,
default: def,
when,
};
return model;
};
export const promptConfirm = <A extends inquirer.Answers>(
name: string,
message: string | ((answers: A) => string),
def: any = undefined,
when: boolean | ((answers: A) => boolean | Promise<boolean>) = true
) => {
const model: ConfirmQuestion<A> = {
type: 'confirm',
name,
message,
default: def,
when,
};
return model;
};

View File

@ -1,23 +0,0 @@
import fs = require('fs');
import path = require('path');
/**
* Remove directory recursively
* Ref https://stackoverflow.com/a/42505874
*/
export const rmdir = (dirPath: string) => {
if (!fs.existsSync(dirPath)) {
return;
}
fs.readdirSync(dirPath).forEach((entry) => {
const entryPath = path.join(dirPath, entry);
if (fs.lstatSync(entryPath).isDirectory()) {
rmdir(entryPath);
} else {
fs.unlinkSync(entryPath);
}
});
fs.rmdirSync(dirPath);
};