DEV Community

Allan Ramos
Allan Ramos

Posted on

Automatizando a criação de arquivos com Plop.js

Repetição que poderia não existir

Não é que eu me esforce muito para tal, mas criar sempre aquela mesma estruturinha e conteúdo base de arquivos é aquele tipo de tarefa que poderia não existir.

Segue abaixo um exemplo do que eu crio quando insiro um novo componente em meu projeto React:

//index.ts
export { MyComponent } from './MyComponent'
export type { MyComponentProps } from './MyComponent'
Enter fullscreen mode Exit fullscreen mode
//MyComponent.tsx
import * as React from 'react'

export type MyComponentProps = {
  name: string
}

export const MyComponent = ({ name }: MyComponentProps) => (
  <p>
    { name }
  </p>
)
Enter fullscreen mode Exit fullscreen mode
//MyComponent.stories.tsx
import * as React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'

import { MyComponent } from '.'
import type { MyComponentProps } from '.'

export default {
  title: 'Components/MyComponent',
  component: MyComponent,
} as ComponentMeta<typeof MyComponent>

const Template: ComponentStory<typeof MyComponent> = (args: MyComponentProps) => (
  <MyComponent {...args} />
)

export const Default = Template.bind({})
Default.args = {
  name: 'João Banana',
}
Enter fullscreen mode Exit fullscreen mode
//MyComponent.test.tsx
import * as React from 'react'
import { render, screen } from '@testing-library/react'

import { MyComponent } from '.'
import type { MyComponentProps } from '.'

const defaultProps = {
  name: 'João Banana',
}

const selectors = {
  name: (name = defaultProps.name) => screen.getByText(name),
}

const renderComponent = (props: MyComponentProps = defaultProps) =>
  render(<MyComponent {...props} />)

  test('should render the component correctly', async () => {
    renderComponent()

    expect(selectors.name()).toBeInTheDocument()
  })
Enter fullscreen mode Exit fullscreen mode

No final das contas o que eu tenho é um componente, arquivo do storybook e arquivo do teste. Por mais que cada componente seja diferente e os testes e suas propriedades mudem, existe uma base de código presente em todos.

Conhecendo o Plop.js

O Plop.js permite criarmos comandos para gerar arquivos com a estrutura que desejarmos.

Basta rodar um npm run plop para ver algo como a imagem abaixo, escrever algumas informações e ter toda aquela estrutura que mostrei anteriormente em uma pasta nova de meu projeto.

plop cli

Instalando

npm install --save-dev plop
Enter fullscreen mode Exit fullscreen mode

Insira o comando abaixo em seu package.json:

"plop": "plop"
Enter fullscreen mode Exit fullscreen mode

Criando nosso gerador de componentes

Na raíz de seu projeto, crie o arquivo plopfile.js com o seguinte conteúdo:

const componentGenerator = require('./plop/component/index')

module.exports = function (plop) {
  componentGenerator(plop)
}
Enter fullscreen mode Exit fullscreen mode

Estamos criando uma função que o Plop.js executará passando o objeto plop como parâmetro. Com esse objeto executaremos funções para criar nosso gerador.

Criando um arquivo gerador base para componentes

O que eu desejo com o Plop.js é que apenas com o nome de meu componente ele crie uma pasta com toda a estrutura de código e arquivos. Para isso, vamos criar um arquivo no caminho plop/component/index.js com o seguinte conteúdo:

module.exports = function (plop) {
  plop.setGenerator('component', {
    description: 'this is a skeleton plopfile',
    prompts: [], // array com perguntas que serão mostradas no terminal
    actions: []  // array de ações para serem executadas após respoder as perguntas
  });
};
Enter fullscreen mode Exit fullscreen mode

Esse é o conteúdo base de um gerador. Você usa a função setGenerator com 2 parâmetros. O primeiro, o nome do gerador, e o segundo, um objeto com 3 opções:

  • description: descrição do gerador;
  • prompts: array com perguntas que serão mostradas no terminal;
  • actions: array de ações para serem executadas após respoder as perguntas.

Sabendo disso, vamos começar a primeira e única pergunta de nosso gerador, que é saber qual o nome do componente:

module.exports = function (plop) {
  plop.setGenerator('component', {
    description: 'React component generator',
    prompts: [
      {
        type: 'input',
        name: 'component_name',
        message: 'Enter the component name: '
      }
    ],
  })
};
Enter fullscreen mode Exit fullscreen mode

plop cli

Com esse código estamos configurando justamente isso que você está vendo. Um input com o texto "Enter the component name:" e com o nome "component_name", que usaremos nas actions.

Para finalizarmos essa parte, agora precisamos definir as ações. O que eu desejo é que sejam criados 4 arquivos diferentes, cada um com sua configuração e código diferente. Para isso, vamos deixar o código assim:

module.exports = function (plop) {
  plop.setGenerator('component', {
    description: 'React component generator',
    prompts: [
      {
        type: 'input',
        name: 'component_name',
        message: 'Enter the component name: '
      }
    ],
    actions: [
      {
        type: 'add',
        path: './src/components/{{pascalCase component_name}}/index.ts',
        templateFile: 'plop/component/index-template.hbs'
      },
      {
        type: 'add',
        path: './src/components/{{pascalCase component_name}}/{{pascalCase component_name}}.tsx',
        templateFile: 'plop/component/component-template.hbs'
      },
      {
        type: 'add',
        path: './src/components/{{pascalCase component_name}}/{{pascalCase component_name}}.stories.tsx',
        templateFile: 'plop/component/stories-template.hbs'
      },
      {
        type: 'add',
        path: './src/components/{{pascalCase component_name}}/{{pascalCase component_name}}.test.tsx',
        templateFile: 'plop/component/test-template.hbs'
      }
    ]
  })
};
Enter fullscreen mode Exit fullscreen mode

Vamos entender cada um das chaves presentes nesse objeto:

  • type: 'add': estou dizendo que desejo adicionar um único arquivo em determinado caminho de meu projeto;
  • path: './src/components/{{pascalCase component_name}}/index.ts': aqui eu informo o caminho de meu arquivo, e perceba uma coisa, como desejo criar o arquivo index.ts dentro de uma nova pasta, eu preciso pegar o nome do componente que foi colocado na CLI, e para isso coloco seu nome(component_name) entre colchetes duplos. Como desejo que a pasta siga o padrão PascalCase, coloco essa informação com espaço antes do valor que desejo formatar.
  • templateFile: 'plop/component/index-template.hbs': o caminho do arquivo de template que será usado para criar esse arquivo.

Criando nossos templates de arquivos

Para usar o código abaixo crie o arquivo index-template.hbs na raíz de plop/component/.

//index-template.hbs
export { {{pascalCase component_name}} } from './{{pascalCase component_name}}'
Enter fullscreen mode Exit fullscreen mode

Os valores obtidos no preenchimento pela CLI também estão disponíveis aqui, e também com funções de formatação.

//component-template.hbs
import * as React from 'react'

export type {{pascalCase component_name}}Props = {
  name: string;
}

export const {{pascalCase component_name}} = ({ name }: {{pascalCase component_name}}Props) => (
  <p>
    { name }
  </p>
)
Enter fullscreen mode Exit fullscreen mode
//stories-template.hbs
import * as React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'

import { {{pascalCase component_name}} } from './{{pascalCase component_name}}'
import type { {{pascalCase component_name}}Props } from './{{pascalCase component_name}}'

export default {
  title: 'Components/{{pascalCase component_name}}',
  component: {{pascalCase component_name}},
} as ComponentMeta<typeof {{pascalCase component_name}}>

const Template: ComponentStory<typeof {{pascalCase component_name}}> = (args: {{pascalCase component_name}}Props) => (
  <{{pascalCase component_name}} {...args} />
)

export const Default = Template.bind({})
Default.args = {
  name: 'João Banana',
}
Enter fullscreen mode Exit fullscreen mode
//test-template.hbs

import * as React from 'react'
import { render, screen } from '@testing-library/react'

import { {{pascalCase component_name}} } from '.'
import type { {{pascalCase component_name}}Props } from '.'

const defaultProps = {
  name: 'João Banana',
}

const selectors = {
  name: (name = defaultProps.name) => screen.getByText(name),
}

const renderComponent = (props: MyComponentProps = defaultProps) =>
  render(<MyComponent {...props} />)

test('should render the component correctly', async () => {
  renderComponent()

  expect(selectors.name()).toBeInTheDocument()
})
Enter fullscreen mode Exit fullscreen mode

Top comments (0)