Commit f458da4d by Alex Khomenko Committed by GitHub

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
parent 1468bab3
...@@ -19,6 +19,7 @@ import { ciBuildPluginTask, ciBuildPluginDocsTask, ciPackagePluginTask, ciPlugin ...@@ -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) => { ...@@ -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
......
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', () => { ...@@ -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",
......
export const testTpl = `
import React from 'react';
import { shallow } from 'enzyme';
import { <%= name %> } from './<%= name %>';
describe('<%= name %>', () => {
it.skip('should render', () => {
});
});
`;
export const componentTpl = `
import React, { FC } from 'react';
interface Props = {};
export const <%= name %>: FC<Props> = (props) => {
return (
<div>Hello world!</div>
)
};
`;
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 %>} />
`;
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';
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 %> />;
};
`;
import _ from 'lodash';
export const pascalCase = _.flow(_.camelCase, _.upperFirst);
...@@ -6,6 +6,8 @@ import { ...@@ -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>( ...@@ -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),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment