Introdução
English version: React and typescript components lib, part 6: code autogeneration with Hygen
Na penúltima parte da série, será adicionada uma nova lib chamada hygen, a qual a partir da definição de um gerador com templates, permite autogerar código com um simples comando no terminal. A ideia vai ser criar um gerador de componente (para gerar a base de um novo componente), dado que todos seguem um certo padrão de estrutura de pastas e nomes de arquivos, além disso, devido a possuir também um padrão de escrita seja da definição do componente em si, da documentação ou dos testes.
Dessa forma o autogerador fica com a preocupação de definir o esqueleto dos novos componentes, enquanto o desenvolvedor se preocupa somente com a definição do comportamento do componente em si.
A ideia é mostrar a parte prática da aplicação na lib de componentes, para uma referência mais aprofundada de como funciona o hygen segue um artigo que escrevi anteriormente sobre o assunto: Reduzindo trabalho manual em React com Hygen
Setup
Primeiro será adicionada a lib do hygen:
yarn add hygen --dev
Após, será realizada a configuração inicial da lib executando no terminal:
npx hygen init self
Esse comando irá gerar a pasta _templates na raiz do projeto, que vai permitir executar o hygen.
Análise geral componente
Antes de criar o gerador de componente, vamos analisar de forma geral como são definidos os componentes dentro da app.
Pela imagem acima, hoje cada componente é definido dentro de uma pasta com o nome dele e em cinco arquivos:
- {nome_do_componente}.tsx: definição do componente
- {nome_do_componente}.test.tsx: definição dos testes do componente
- {nome_do_componente}.stories.tsx: definição dos cenários que vão parecer na documentação do componente
- {nome_do_componente}.mdx: definição da documentação do componente
- index.ts: definição do export do componente dentro da pasta dele
Além dos arquivos internos da pasta com o nome do componente, dentro da pasta src/components, no arquivo index.ts é definido o export de cada componente que será disponibilizado pela lib.
Logo o gerador vai ter que ter um template para cada arquivo comentado acima.
Gerador de componentes
Uma vez feita a análise do que define os componentes da lib, agora seguiremos com a criação do gerador de componente.
Para criar um novo gerador será executado no terminal:
npx hygen generator new component
Resolvi definir o nome de gerador de component para deixar claro o contexto dele. Após executar o comando acima vai ser criado automaticamente uma pasta component/new dentro da pasta _templates. Dentro da pasta _templates/component/new vai ser onde iremos definir os templates, dentro dela já tem um arquivo hello.ejs.t de exemplo, que iremos remover pois não iremos utilizar ao executar o gerador.
Uma vez que criamos o gerador, agora é hora de definir os templates
que ele vai usar como base para autogerar código.
Templates
Para definição dos templates, vou usar como base os arquivos do componente Text.
- Primeiro template: componente em si
Para o primeiro template, vai ser levado como base o arquivo Text.tsx:
import React from "react"; import styled from "styled-components"; export interface TextProps { children: React.ReactNode; color?: string; weight?: "normal" | "bold"; fontWeight?: number; fontSize?: string; fontFamily?: string; } export interface StyledTextProps { $color?: string; $weight?: "normal" | "bold"; $fontWeight?: number; $fontSize?: string; $fontFamily?: string; } export const StyledText = styled.span<StyledTextProps>` ${(props) => props.$color && `color: ${props.$color};`} ${(props) => props.$fontSize && `font-size: ${props.$fontSize};`} font-weight: ${(props) => props.$fontWeight ? props.$fontWeight : props.$weight ? props.$weight : "normal"}; ${(props) => props.$fontFamily && `font-family: ${props.$fontFamily};`} `; const Text = ({ children, color = "#000", weight = "normal", fontWeight, fontSize = "16px", fontFamily, }: TextProps) => ( <StyledText $color={color} $weight={weight} $fontWeight={fontWeight} $fontSize={fontSize} $fontFamily={fontFamily} > {children} </StyledText> ); export default Text; Dele pode se notar que o que vai ter em comum entre os componentes vai ser os imports, definição de types do componente com nome {nome do componente}Props, definição de types do styled-components com nome Styled{nome do componente}Props, definição de propriedades css com styled-components com nome Styled{nome do componente} e no fim a definição e export default do componente.
Tendo em vista esses pontos, dentro da pasta _templates/component/new vai ser criado o arquivo component.ejs.t, que vai corresponder a criação do esqueleto do componente em si:
--- to: src/components/<%=name%>/<%=name%>.tsx --- import React from "react"; import styled from "styled-components"; export interface <%=name%>Props { } export interface Styled<%=name%>Props { } export const Styled<%=name%> = styled.<%=html%><Styled<%=name%>Props>` `; const <%=name%> = ({ }: <%=name%>Props) => ( <Styled<%=name%> > </Styled<%=name%>> ); export default <%=name%>; <%=name%> e <%=html%> correspondem a valores dinâmicos que vão ser passados ao executar o gerador, para substituir no código acima, correspondendo respectivamente ao nome do componente e ao elemento html que ele corresponde.
Em to: é definido onde o arquivo autogerado a partir desse template vai ser criado. Abaixo dele, o código que vai ser gerado dentro do arquivo.
Esse template traz os imports necessários e o esqueleto que define um componente dentro da app, dado os pontos que foram enumerados acima.
- Segundo template: testes do componente
Para o segundo template, vai ser levado como base o arquivo Text.test.tsx:
import React from "react"; import "@testing-library/jest-dom"; import "jest-styled-components"; import { render, screen } from "@testing-library/react"; import Text from "./Text"; describe("<Text />", () => { it("should render component with default properties", () => { render(<Text>Text</Text>); const element = screen.getByText("Text"); expect(element).toBeInTheDocument(); expect(element).toHaveStyleRule("color", "#000"); expect(element).toHaveStyleRule("font-size", "16px"); expect(element).toHaveStyleRule("font-weight", "normal"); }); it("should render component with custom color", () => { render(<Text color="#fff">Text</Text>); expect(screen.getByText("Text")).toHaveStyleRule("color", "#fff"); }); it("should render component with bold weight", () => { render(<Text weight="bold">Text</Text>); expect(screen.getByText("Text")).toHaveStyleRule("font-weight", "bold"); }); it("should render component with custom weight", () => { render(<Text fontWeight={500}>Text</Text>); expect(screen.getByText("Text")).toHaveStyleRule("font-weight", "500"); }); it("should render component with custom font size", () => { render(<Text fontSize="20px">Text</Text>); expect(screen.getByText("Text")).toHaveStyleRule("font-size", "20px"); }); it("should render component with custom font family", () => { render(<Text fontFamily="TimesNewRoman">Text</Text>); expect(screen.getByText("Text")).toHaveStyleRule( "font-family", "TimesNewRoman", ); }); }); Dele pode se notar que o que vai ter em comum entre os componentes vai ser os imports, o describe com o nome do componente, o primeiro teste com a análise das propriedades default do componente.
Tendo em vista esses pontos, dentro da pasta _templates/component/new vai ser criado o arquivo test.ejs.t, que vai corresponder a criação do esqueleto do teste em si:
--- to: src/components/<%=name%>/<%=name%>.test.tsx --- import React from "react"; import "@testing-library/jest-dom"; import "jest-styled-components"; import { render, screen } from "@testing-library/react"; import <%=name%> from "./<%=name%>"; describe("<<%=name%> />", () => { it("should render component with default properties", () => { }); }); Esse template traz os imports necessários e o esqueleto que define o arquivo de teste dentro da app, dado os pontos que foram enumerados acima.
- Terceiro template: cenários de documentação
Para o terceiro template, vai ser levado como base o arquivo Text.stories.tsx:
import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; import Text from "./Text"; import StorybookContainer from "../StorybookContainer/StorybookContainer"; const meta: Meta<typeof Text> = { title: "Text", component: Text, argTypes: {}, }; export default meta; type Story = StoryObj<typeof Text>; export const Default: Story = { args: {}, render: (args) => ( <StorybookContainer> <Text {...args}>Text</Text> </StorybookContainer> ), }; export const PredefinedFontWeight: Story = { args: {}, render: (args) => ( <StorybookContainer> <Text {...args}>Text</Text> <Text {...args} weight="bold"> Text </Text> </StorybookContainer> ), }; export const Color: Story = { args: {}, render: (args) => ( <StorybookContainer> <Text {...args}>Text</Text> <Text {...args} color="#800080"> Text </Text> </StorybookContainer> ), }; export const CustomFontWeight: Story = { args: {}, render: (args) => ( <StorybookContainer> <Text {...args}>Text</Text> <Text {...args} fontWeight={900}> Text </Text> </StorybookContainer> ), }; export const FontSize: Story = { args: {}, render: (args) => ( <StorybookContainer> <Text {...args}>Text</Text> <Text {...args} fontSize="30px"> Text </Text> </StorybookContainer> ), }; export const FontFamily: Story = { args: {}, render: (args) => ( <StorybookContainer> <Text {...args}>Text</Text> <Text {...args} fontFamily="Arial"> Text </Text> </StorybookContainer> ), }; Dele pode se notar que o que vai ter em comum entre os componentes vai ser os imports, a definição de meta e type Story com o nome do componente, e o primeiro cenário com o cenário default do componente.
Tendo em vista esses pontos, dentro da pasta _templates/component/new vai ser criado o arquivo stories.ejs.t, que vai corresponder a criação do esqueleto dos cenários de documentação em si:
--- to: src/components/<%=name%>/<%=name%>.stories.tsx --- import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; import <%=name%> from "./<%=name%>"; import StorybookContainer from "../StorybookContainer/StorybookContainer"; const meta: Meta<typeof <%=name%>> = { title: "<%=name%>", component: <%=name%>, argTypes: {}, }; export default meta; type Story = StoryObj<typeof <%=name%>>; export const Default: Story = { args: {}, render: (args) => ( <StorybookContainer> <<%=name%> {...args} /> </StorybookContainer> ), }; Esse template traz os imports necessários e o esqueleto que define o arquivo de cenários de documentação dentro da app, dado os pontos que foram enumerados acima.
- Quarto template: documentação do componente
Para o quarto template, vai ser levado como base o arquivo Text.mdx:
import { Canvas, Controls, Meta } from "@storybook/blocks"; import * as Stories from "./Text.stories"; <Meta of={Stories} /> # Text Text base component. <Canvas of={Stories.Default} withToolbar /> <Controls of={Stories.Default} /> ## Predefined properties ### Font Weight There are two font weight predefined properties: normal(default) and bold. <Canvas of={Stories.PredefinedFontWeight} withToolbar /> ## Custom properties ### Color Text color can be modified. <Canvas of={Stories.Color} withToolbar /> ### Font Weight Text font weight can be modified. <Canvas of={Stories.CustomFontWeight} withToolbar /> ### Font Size Text font size can be modified. <Canvas of={Stories.FontSize} withToolbar /> ### Font Family Text font family can be modified. <Canvas of={Stories.FontFamily} withToolbar /> Dele pode se notar que o que vai ter em comum entre os componentes vai ser os imports, a definição de Meta, descrição inicial com nome do componente, Canvas e Controls com cenário default do compoenente, seção de Predefined properties e seção de Custom properties.
Tendo em vista esses pontos, dentro da pasta _templates/component/new vai ser criado o arquivo doc.ejs.t, que vai corresponder a criação do esqueleto da documentação do componente:
--- to: src/components/<%=name%>/<%=name%>.mdx --- import { Canvas, Controls, Meta } from "@storybook/blocks"; import * as Stories from "./<%=name%>.stories"; <Meta of={Stories} /> # <%=name%> <%=name%> base component. <Canvas of={Stories.Default} withToolbar /> <Controls of={Stories.Default} /> ## Predefined properties ## Custom properties Esse template traz os imports necessários e o esqueleto que define a documentação do componente dentro da app, dado os pontos que foram enumerados acima.
- Quinto template: export do componente dentro da pasta dele
Para o quinto template, vai ser levado como base o arquivo index.ts dentro da pasta src/components/Text:
export { default } from "./Text"; Dentro da pasta _templates/component/new vai ser criado o arquivo componentIndex.ejs.t, que vai corresponder a criação do export do componente interno a sua pasta:
--- to: src/components/<%=name%>/index.ts --- export { default } from "./<%=name%>"; Esse template traz o export do componente interno a pasta dele.
- Sexto template: export do componente que vai disponibilizar ele para quem adicionar a lib
Para o sexto template, não vai ser considerado um arquivo como base, mas sim vai ser adicionado uma linha a um arquivo já existente dentro da app, que corresponde ao index.ts dentro da pasta src/components:
export { default as Tag } from "./Tag"; export { default as Text } from "./Text"; Dentro da pasta _templates/component/new vai ser criado o arquivo index.ejs.t, que vai corresponder a adição do export do novo componente dentro do arquivo já existente:
--- inject: true to: src/components/index.ts at_line: 0 --- export { default as <%=name%> } from "./<%=name%>"; Nele já se tem duas coisas diferentes em relação aos outros arquivos de template. inject: true diz que não vai ser gerado um novo arquivo, mas sim vai ser feita injeção de código dentro de um existente, at_line: 0 diz que vai ser adicionado o código na primeira linha.
Uma vez definido todos os templates, dentro de package.json vai ser definido o script para executar o gerador:
"scripts": { //... "create-component": "hygen component new" Nele se define a execução do gerador component usando o hygen.
Exemplo de uso
Para testar o funcionamento do gerador, vai ser executado no terminal:
yarn create-component Button --html button
Nesse comando está se executando o gerador de componente e passando para onde tiver <%=name%> substituir por Button e onde tiver <%=html%> substituir por button.
Ao executar no terminal, ele vai informar que foram gerados cinco arquivos e que teve injeção de código em um arquivo já existente:
Ficando a estrutura do novo componente Button da seguinte forma:
- Button.tsx
import React from "react"; import styled from "styled-components"; export interface ButtonProps { } export interface StyledButtonProps { } export const StyledButton = styled.button<StyledButtonProps>` `; const Button = ({ }: ButtonProps) => ( <StyledButton > </StyledButton> ); export default Button; - Button.test.tsx
import React from "react"; import "@testing-library/jest-dom"; import "jest-styled-components"; import { render, screen } from "@testing-library/react"; import Button from "./Button"; describe("<Button />", () => { it("should render component with default properties", () => { }); }); - Button.stories.tsx
import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; import Button from "./Button"; import StorybookContainer from "../StorybookContainer/StorybookContainer"; const meta: Meta<typeof Button> = { title: "Button", component: Button, argTypes: {}, }; export default meta; type Story = StoryObj<typeof Button>; export const Default: Story = { args: {}, render: (args) => ( <StorybookContainer> <Button {...args} /> </StorybookContainer> ), }; - Button.mdx
import { Canvas, Controls, Meta } from "@storybook/blocks"; import * as Stories from "./Button.stories"; <Meta of={Stories} /> # Button Button base component. <Canvas of={Stories.Default} withToolbar /> <Controls of={Stories.Default} /> ## Predefined properties ## Custom properties - index.ts (interno a pasta src/components/Button)
export { default } from "./Button"; - index.ts (interno a pasta src/components)
export { default as Button } from "./Button"; export { default as Tag } from "./Tag"; export { default as Text } from "./Text"; Autogerando dessa forma todos os arquivos que servem de base para a definição de um componente, com os exports definidos também.
package.json
Será mudada a versão dentro de package.json para 0.6.0, uma vez que uma nova versão da lib será disponibilizada:
{ "name": "react-example-lib", "version": "0.6.0", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "types": "dist/index.d.ts", "files": [ "dist" ], "license": "MIT", "repository": { "type": "git", "url": "https://github.com/griseduardo/react-example-lib.git" }, "scripts": { "build": "rollup -c --bundleConfigAsCjs", "lint-src": "eslint src", "lint-src-fix": "eslint src --fix", "lint-fix": "eslint --fix", "format-src": "prettier src --check", "format-src-fix": "prettier src --write", "format-fix": "prettier --write", "test": "jest", "prepare": "husky", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "create-component": "hygen component new" }, "lint-staged": { "src/components/**/*.{ts,tsx}": [ "yarn lint-fix", "yarn format-fix" ], "src/components/**/*.tsx": "yarn test --findRelatedTests --bail" }, "devDependencies": { "@babel/core": "^7.26.10", "@babel/preset-env": "^7.26.9", "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", "@eslint/js": "^9.19.0", "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "11.1.6", "@storybook/addon-essentials": "^8.6.12", "@storybook/addon-interactions": "^8.6.12", "@storybook/addon-onboarding": "^8.6.12", "@storybook/blocks": "^8.6.12", "@storybook/builder-vite": "^8.6.12", "@storybook/react": "^8.6.12", "@storybook/react-vite": "^8.6.12", "@storybook/test": "^8.6.12", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "chromatic": "^12.0.0", "eslint": "^9.19.0", "eslint-plugin-react": "^7.37.4", "eslint-plugin-storybook": "^0.12.0", "husky": "^9.1.7", "hygen": "^6.2.11", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-styled-components": "^7.2.0", "lint-staged": "^15.5.0", "prettier": "^3.4.2", "react": "^19.0.0", "react-dom": "^19.0.0", "rollup": "^4.30.1", "rollup-plugin-dts": "^6.1.1", "rollup-plugin-peer-deps-external": "^2.2.4", "storybook": "^8.6.12", "styled-components": "^6.1.14", "typescript": "^5.7.3", "typescript-eslint": "^8.23.0", "vite": "^6.3.5" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", "styled-components": "^6.1.14" }, "eslintConfig": { "extends": [ "plugin:storybook/recommended" ] } } Arquivo CHANGELOG
Como uma nova versão será disponibilizada, será adicionado em CHANGELOG.md sobre o que foi modificado:
## 0.6.0 _Jun. 30, 2025_ - add hygen - add component generator ## 0.5.0 _May. 29, 2025_ - change Tag and Text default behavior - add storybook - add Tag and Text storybook docs ## 0.4.0 _Abr. 29, 2025_ - setup husky and lint-staged - define pre-commit actions ## 0.3.0 _Mar. 24, 2025_ - setup jest and testing-library - add components tests ## 0.2.0 _Fev. 24, 2025_ - setup typescript-eslint and prettier - add custom rules ## 0.1.0 _Jan. 29, 2025_ - initial config Estrutura de pastas
A estrutura de pastas ficará da seguinte forma:
Publicação nova versão
Resolvi apagar a pasta Button e remover o export do Button dentro de index.ts (da pasta src/components), uma vez que foi usado para exemplificação do uso do hygen mas não trabalhado o componente em si.
É preciso ver se a execução do rollup ocorre com sucesso antes da publicação. Para isso será executado o yarn build no terminal, que foi definido em package.json.
Executando com sucesso, é realizar a publicação da nova versão da lib: npm publish --access public
Conclusão
A ideia desse artigo foi criar um gerador de componentes dentro da lib, a partir da definição de templates, com o objetivo de agilizar a criação de novos componentes, sendo o gerador responsável pela autogeração do esqueleto de um novo componente.
Segue o repositório no github e a lib no npmjs com as novas modificações.




Top comments (0)