DEV Community

Eduardo Henrique Gris
Eduardo Henrique Gris

Posted on

Biblioteca de componentes React e typescript, parte 6: autogeração de código com Hygen

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.

Image description

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;
Enter fullscreen mode Exit fullscreen mode

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%>;
Enter fullscreen mode Exit fullscreen mode

<%=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",
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

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", () => {
  });
});
Enter fullscreen mode Exit fullscreen mode

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>
  ),
};
Enter fullscreen mode Exit fullscreen mode

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>
  ),
};
Enter fullscreen mode Exit fullscreen mode

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 />
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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%>";
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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%>";
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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:

Image description

Ficando a estrutura do novo componente Button da seguinte forma:

Image description

  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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", () => {
  });
});
Enter fullscreen mode Exit fullscreen mode
  • 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>
  ),
};
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • index.ts (interno a pasta src/components/Button)
export { default } from "./Button";
Enter fullscreen mode Exit fullscreen mode
  • 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";
Enter fullscreen mode Exit fullscreen mode

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"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Estrutura de pastas

A estrutura de pastas ficará da seguinte forma:

Image description

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)