mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 10:49:40 +08:00
Toolkit: Remove plugin:create and component:create commands (#66729)
This commit is contained in:
@ -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
|
||||
|
@ -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);
|
@ -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);
|
@ -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);
|
||||
}
|
||||
};
|
@ -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', () => {
|
||||
|
||||
});
|
||||
});
|
||||
`;
|
@ -1,10 +0,0 @@
|
||||
export const componentTpl = `import React from 'react';
|
||||
|
||||
export interface Props {};
|
||||
|
||||
export const <%= name %> = (props: Props) => {
|
||||
return (
|
||||
<div>Hello world!</div>
|
||||
)
|
||||
};
|
||||
`;
|
@ -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 %>} />
|
||||
`;
|
@ -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';
|
@ -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 %> />;
|
||||
};
|
||||
`;
|
@ -1,3 +0,0 @@
|
||||
import { flow, camelCase, upperFirst } from 'lodash';
|
||||
|
||||
export const pascalCase = flow([camelCase, upperFirst]);
|
@ -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;
|
||||
};
|
@ -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);
|
||||
};
|
Reference in New Issue
Block a user