mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 07:32:57 +08:00
React generator (#23150)
* Toolkit: Setup templates * Toolkit: Add plop * Toolkit: Setup createComponent task * Toolkit: Use lodash templates * Toolkit: Generate story and mdx file * Toolkit: Add story type * Toolkit: Fix types * Toolkit: Add test template * Toolkit: Remove plop * Toolkit: Tweak types * Toolkit: Minor fixes * Toolkit: Add internal story option * Toolkit: Fix test * Toolkit: Clarify prompt * Toolkit: add prompt list for component group * Toolkit: make generator script internal * Toolkit: add description * Toolkit: add missing when condition
This commit is contained in:
@ -19,6 +19,7 @@ import { ciBuildPluginTask, ciBuildPluginDocsTask, ciPackagePluginTask, ciPlugin
|
|||||||
import { buildPackageTask } from './tasks/package.build';
|
import { buildPackageTask } from './tasks/package.build';
|
||||||
import { pluginCreateTask } from './tasks/plugin.create';
|
import { pluginCreateTask } from './tasks/plugin.create';
|
||||||
import { bundleManagedTask } from './tasks/plugin/bundle.managed';
|
import { bundleManagedTask } from './tasks/plugin/bundle.managed';
|
||||||
|
import { componentCreateTask } from './tasks/component.create';
|
||||||
|
|
||||||
export const run = (includeInternalScripts = false) => {
|
export const run = (includeInternalScripts = false) => {
|
||||||
if (includeInternalScripts) {
|
if (includeInternalScripts) {
|
||||||
@ -114,6 +115,16 @@ export const run = (includeInternalScripts = false) => {
|
|||||||
milestone: cmd.milestone,
|
milestone: cmd.milestone,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// React generator
|
||||||
|
program
|
||||||
|
.command('component:create')
|
||||||
|
.description(
|
||||||
|
'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 () => {
|
||||||
|
await execTask(componentCreateTask)({});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
program
|
program
|
||||||
|
86
packages/grafana-toolkit/src/cli/tasks/component.create.ts
Normal file
86
packages/grafana-toolkit/src/cli/tasks/component.create.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { Task, TaskRunner } from './task';
|
||||||
|
import fs from 'fs';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { prompt } from 'inquirer';
|
||||||
|
import { pascalCase } from '../utils/pascalCase';
|
||||||
|
import { promptConfirm, promptInput, promptList } from '../utils/prompt';
|
||||||
|
import { componentTpl, docsTpl, storyTpl, testTpl } from '../templates';
|
||||||
|
|
||||||
|
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);
|
@ -8,6 +8,7 @@ describe('Manifest', () => {
|
|||||||
"changelog.ts",
|
"changelog.ts",
|
||||||
"cherrypick.ts",
|
"cherrypick.ts",
|
||||||
"closeMilestone.ts",
|
"closeMilestone.ts",
|
||||||
|
"component.create.ts",
|
||||||
"core.start.ts",
|
"core.start.ts",
|
||||||
"manifest.test.ts",
|
"manifest.test.ts",
|
||||||
"manifest.ts",
|
"manifest.ts",
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
export const testTpl = `
|
||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { <%= name %> } from './<%= name %>';
|
||||||
|
|
||||||
|
|
||||||
|
describe('<%= name %>', () => {
|
||||||
|
it.skip('should render', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`;
|
@ -0,0 +1,11 @@
|
|||||||
|
export const componentTpl = `
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
interface Props = {};
|
||||||
|
|
||||||
|
export const <%= name %>: FC<Props> = (props) => {
|
||||||
|
return (
|
||||||
|
<div>Hello world!</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
`;
|
@ -0,0 +1,16 @@
|
|||||||
|
export const docsTpl = `import { Story, Preview, Props } from '@storybook/addon-docs/blocks';
|
||||||
|
import { <%= name %> } from './<%= name %>';
|
||||||
|
|
||||||
|
# <%= name %>
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
\`\`\`jsx
|
||||||
|
import { <%= name %> } from '@grafana/ui';
|
||||||
|
|
||||||
|
<<%= name %> />
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Props
|
||||||
|
<Props of={<%= name %>} />
|
||||||
|
`;
|
4
packages/grafana-toolkit/src/cli/templates/index.ts
Normal file
4
packages/grafana-toolkit/src/cli/templates/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
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';
|
@ -0,0 +1,22 @@
|
|||||||
|
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 single = () => {
|
||||||
|
return <<%= name %> />;
|
||||||
|
};
|
||||||
|
`;
|
3
packages/grafana-toolkit/src/cli/utils/pascalCase.ts
Normal file
3
packages/grafana-toolkit/src/cli/utils/pascalCase.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export const pascalCase = _.flow(_.camelCase, _.upperFirst);
|
@ -6,6 +6,8 @@ import {
|
|||||||
PasswordQuestion,
|
PasswordQuestion,
|
||||||
EditorQuestion,
|
EditorQuestion,
|
||||||
ConfirmQuestion,
|
ConfirmQuestion,
|
||||||
|
ListQuestion,
|
||||||
|
ChoiceOptions,
|
||||||
} from 'inquirer';
|
} from 'inquirer';
|
||||||
|
|
||||||
type QuestionWithValidation<A = any> =
|
type QuestionWithValidation<A = any> =
|
||||||
@ -40,6 +42,25 @@ export const promptInput = <A>(
|
|||||||
return required ? answerRequired(model) : model;
|
return required ? answerRequired(model) : model;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const promptList = <A>(
|
||||||
|
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>(
|
export const promptConfirm = <A>(
|
||||||
name: string,
|
name: string,
|
||||||
message: string | ((answers: A) => string),
|
message: string | ((answers: A) => string),
|
||||||
|
Reference in New Issue
Block a user