This content originally appeared on DEV Community and was authored by Eduardo Henrique Gris
Introduction
Portuguese version: Biblioteca de componentes React e typescript, parte 6: autogeração de código com Hygen
In the second-to-last part of the series, a new library called hygen
will be added. By defining a generator with templates, Hygen allows you to auto-generate code with a simple terminal command. The idea is to create a component generator (to generate the base of a new component), since all components follow a certain folder and file naming structure. Moreover, there’s also a consistent writing pattern, whether for the component definition itself, its documentation or its tests.
This way, the generator handles the responsibility of setting up the skeleton for new components, while the developer can focus solely on defining the component’s actual behavior.
The goal is to demonstrate the practical application of this in the component library. For a more in-depth reference on how hygen
works, here’s an article I previously wrote on the topic: Reducing manual work in React with Hygen.
Setup
First, the hygen library will be added:
yarn add hygen --dev
Next, the initial setup of the library will be done by running the following command in the terminal:
npx hygen init self
This command will generate a _templates
folder at the root of the project, which enables hygen to run.
General Component Analysis
Before creating the component generator, let’s take a general look at how components are defined within the app.
Based on the image above, each component is currently defined inside a folder named after the component itself and consists of five files:
- {component_name}.tsx: the component definition
- {component_name}.test.tsx: the component’s test definition
- {component_name}.stories.tsx: the scenarios that will appear in the component’s documentation
- {component_name}.mdx: the component’s documentation
- index.ts: defines the export of the component within its own folder
In addition to the internal files inside the component’s folder, there’s also a central index.ts
file located in the src/components
directory. This file defines the exports for each component that will be made available by the library.
Therefore, the generator will need to have a template for each of the files listed above.
Component generator
Once we’ve analyzed what defines the components in the library, we can now move forward with creating the component generator.
To create a new generator, run the following command in the terminal:
npx hygen generator new component
I chose to name the generator component
to make its purpose clear. After running the command above, a component/new
folder will automatically be created inside the _templates
directory. This is where we’ll define the templates.
Inside _templates/component/new
, there will already be a sample file called hello.ejs.t
, which we’ll remove since it won’t be used when running the generator.
Once the generator is created, it’s time to define the templates that will serve as the base for autogenerating code.
Templates
For defining the templates, I will use the files from the Text component as a base.
- First template: the component itself
For the first template, the Text.tsx
file will be used as the base:
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;
From it, we can notice that what the components will have in common are the imports, the definition of the component’s types named {component name}Props
, the styled-components types named Styled{component name}Props
, the CSS properties defined with styled-components named Styled{component name}
, and finally, the component’s definition and its default export.
With these points in mind, inside the _templates/component/new
folder, a file named component.ejs.t
will be created, which will correspond to the creation of the component’s skeleton itself:
---
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%> and <%=html%> correspond to dynamic values that will be passed when running the generator, replacing those placeholders in the code above with, respectively, the component’s name and the html element it represents.
The to:
field defines where the autogenerated file based on this template will be created. Below it is the code that will be generated inside the file.
This template includes the necessary imports and the skeleton that defines a component within the app, following the points outlined above.
- Second template: component tests
For the second template, the Text.test.tsx
file will be used as the base:
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",
);
});
});
From it, we can notice that what the components will have in common are the imports, the describe
block with the component’s name, and the first test checking the component’s default props.
With these points in mind, inside the _templates/component/new
folder, a file named test.ejs.t
will be created, which will correspond to the creation of the test skeleton itself:
---
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", () => {
});
});
This template includes the necessary imports and the skeleton that defines the test file within the app, based on the points outlined above.
- Third template: documentation scenarios
For the third template, the Text.stories.tsx
file will be used as the base:
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>
),
};
From it, we can observe that what the components will have in common are the imports, the meta
definition and the type Story
with the component’s name, and the first scenario featuring the component’s default story.
With these points in mind, inside the _templates/component/new
folder, a file named stories.ejs.t
will be created, which will correspond to the creation of the documentation scenarios skeleton itself:
---
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>
),
};
This template includes the necessary imports and the skeleton that defines the documentation scenarios file within the app, based on the points outlined above.
- Fourth template: component documentation
For the fourth template, the Text.mdx
file will be used as the base:
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 />
From it, we can see that what the components will have in common are the imports, the Meta
definition, an initial description with the component’s name, the Canvas and Controls displaying the component’s default scenario, a section for Predefined Properties
and a section for Custom Properties
.
With these points in mind, inside the _templates/component/new
folder, a file named doc.ejs.t
will be created, which will correspond to the creation of the component’s documentation skeleton:
---
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
This template includes the necessary imports and the skeleton that defines the component’s documentation within the app, based on the points outlined above.
- Fifth template: component export inside its folder
For the fifth template, the index.ts
file inside the src/components/Text
folder will be used as the base:
export { default } from "./Text";
Inside the _templates/component/new
folder, a file named componentIndex.ejs.t
will be created, which will correspond to the creation of the component’s export inside its own folder:
---
to: src/components/<%=name%>/index.ts
---
export { default } from "./<%=name%>";
This template contains the export of the component inside its own folder.
- Sixth template: component export to make it available for library users
For the sixth template, instead of basing it on an existing file, a line will be added to an existing file within the app — specifically, the index.ts
file inside the src/components
folder:
export { default as Tag } from "./Tag";
export { default as Text } from "./Text";
Inside the _templates/component/new
folder, a file named index.ejs.t
will be created, which will correspond to adding the export of the new component within the existing file:
---
inject: true
to: src/components/index.ts
at_line: 0
---
export { default as <%=name%> } from "./<%=name%>";
This template already has two differences compared to the other template files. inject: true
means that a new file will not be created; instead, code will be injected into an existing file. at_line: 0
specifies that the code will be added at the very first line.
Once all the templates are defined, a script to run the generator will be added inside the package.json
:
"scripts": {
//...
"create-component": "hygen component new"
It defines the execution of the component
generator using hygen
.
Example of use
To test the generator’s functionality, run the following command in the terminal:
yarn create-component Button --html button
In this command, the component generator is being executed. It will replace every occurrence of <%= name %>
with Button
and <%= html %>
with button
.
When executed in the terminal, it will indicate that five files were generated and that code was injected into an existing file:
The structure of the new Button component will look like this:
- 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";
Automatically generating all the files that serve as the base for defining a component in this way, with the exports also defined.
package.json
The version inside package.json will be changed to 0.6.0
, since a new version of the library will be released:
{
"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"
]
}
}
CHANGELOG file
Since a new version will be released, the CHANGELOG.md
will be updated with what has been changed:
## 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
Folders structure
The folder structure will be as follows:
Publication of a new version
I decided to delete the Button
folder and remove the Button export from index.ts
(inside the src/components folder), since it was used only to illustrate the usage of hygen but the component itself was not worked on.
It is necessary to check if the rollup build runs successfully before publishing. For that, run yarn build
in the terminal, as defined in package.json
.
If it runs successfully, proceed to publish the new version of the library with: npm publish --access public
Conclusion
The idea of this article was to create a component generator within the library, based on template definitions, with the goal of speeding up the creation of new components. The generator is responsible for automatically generating the skeleton of a new component.Here is the repository on github and the library on npmjs with the new modifications.
This content originally appeared on DEV Community and was authored by Eduardo Henrique Gris