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)