Executando verificação de segurança...
2

Definindo um bom code style com Prettier, Eslint e ESBuild

Garantir um estilo de código homogêneo para diversas bases de código em diferentes repositórios pode ser uma grande preocupação em muitas esquipes de desenvolvimento de software.

Abaixo demonstro como é possível organizar de forma simples o code style utilizando ferramentas como prettier em conjunto com ESList, EditorConfig e ESBuild para criar projetos robustos com bases de código muito bem formatadas e validadas.

Depois de seguir esse tutorial, você pode simplesmente copiar e colar os arquivos desse projeto dentro de qualquer projeto seu e desde que seja um projeto javascript ou typescript, pimba... todas as formatações vão funcionar.

Dependências

O primeiro passo é criar um diretório para o projeto via terminal.

mkdir meu-projeto
cd meu-projeto

Agora podemos iniciar um arquivo de gerenciamento de pacotes com PNPM.

pnpm init

No passo seguinte vamos instalar as dependências de desenvolvimento.

pnpm add -D esbuild chokidar live-server esbuild-plugin-path-alias esbuild-plugin-eslint esbuild-copy-files-plugin

Com essas dependências podemos criar um ambiente de desenvolvimento incrível com ESBuild e seus plugins.

Configurando o VSCode com Prettier e EditorConfig

Para garantir que as formatações de lint e estilos de código sejam aplicadas automaticamente sempre que um arquivo for modificado no projeto os seguintes arquivos e configurações são necessários.

Crie os arquivos abaixo na raiz do projeto:

.editorconfig

# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
max_line_length = 120

[*.md]
trim_trailing_whitespace = false

.eslintrc

{
  "env": {
    "browser": true,
    "es6": true
  },
  "parserOptions": {
    "ecmaVersion": 2021,
    "sourceType": "module"
  },
  "rules": {
    "semi": ["error", "never"],
    "quotes": ["error", "single"],
    "comma-dangle": ["error", "never"],
    "no-implicit-globals": "off",
    "no-new-require":"off",
    "max-len":0,
     "arrow-parens": "off"
  },
  "plugins": ["fetch"],
  "extends": ["eslint:recommended", "plugin:fetch/recommended"],
  "ignorePatterns": ["esbuild.config.js"]
}

.prettierrc

{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "none",
  "maxLen": 120,
  "arrowParens": "avoid"
}

jsconfig.json

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@/components/*": ["components/*"],
      "@/utils/*": ["utils/*"],
      "@/services/*": ["services/*"],
      "@/assets/*": ["assets/*"]
    }
  }
}

Você pode entender a fundo cada uma das configurações e mais, visitando os links no final desse artigo. Toda via, cada um desses arquivos tem a responsabilidade de oferecer uma formatação automática de alguma característica bacana de lint e code style.

O arquivo .editorConfig por exemplo define o caractere de identação, o tamanho de identação, o tamanho máximo de cada linha em caracteres e se é necessário aplicar ponto virgula no fim da linha ou não.

Nos arquivos .prettierrc e .eslintrc temos configurações muito semelhantes que vão apoiar essas configurações do .editorconfig em momentos diferentes.

No caso do prettier, sempre que uma alteração ocorrer no código de um arquivo, o prettier utilizará as instruções fornecidas para aplicar as regras de formatação automaticamente quando o arquivo for salvo.

O eslint, aplicará as regras definidas acima sempre que um build do projeto for acionado. As saídas podem ser visualizadas tanto no próprio vscode como através do terminal onde o projeto foi iniciado.

Por fim, o arquivo jsconfig.json define alguns diretórios ou caminhos de importação padrão. Dessa forma, é possível simplificar a forma como componentes, services, utilitários e assets são importados. Porém, para funcionar adequadamente, será necessário configurar o sistema de build.

Sistema de build com ESBuild

Primeiro vamos importar todos os plugins e bibliotecas que vamos incluir no arquivo esbuild.config.js na raiz do projeto, para que tudo funcione adequadamente.

  //esbuild.config.js

import { build } from 'esbuild'
import chokidar from 'chokidar'
import liveServer from 'live-server'

import path from 'path'
import aliasPlugin from 'esbuild-plugin-path-alias'
import eslint from 'esbuild-plugin-eslint'
import copy from 'esbuild-copy-files-plugin'

import { fileURLToPath } from 'url'
import { dirname } from 'path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)  

Com todas as dependências necessárias importadas podemos seguir com as configurações do modo de desenvolvimento e do servidor de desenvolvimento.

//Código omitido
;(async () => {
  const isDevMode = () => {
    return process.env.NODE_ENV === 'development'
  }

  const liveServerCors = (req, res, next) => {
    res.setHeader('Access-control-allow-origin', '*')
    next()
  }
)()

Observe que ativei o CORS, permitindo que requisições de diferentes origens sejam permitidas, isso pode ser necessário se você estiver trabalhando com uma estrutura de Micro Front End.

O próximo passo é configurar os plugins que utilizaremos no eslint para ensiná-lo a entender os paths padrões de importação de módulos definidos no jsconfig.json.

Também é necessário definir o plugin de copia de arquivos e o plugin que aplicará as configurações de linter do ESLint que foram configuradas no arquivio .eslintrc.

//Código omitido
const plugins = [
  aliasPlugin({
    '@/components': path.resolve(__dirname, './src/components'),
    '@/services': path.resolve(__dirname, './src/services'),
    '@/utils': path.resolve(__dirname, './src/utils'),
    '@/assets': path.resolve(__dirname, './src/assets')
  }),
  copy({
    source: ['./src/index.html', './src/config.modules.js'],
    target: 'dist',
    copyWithFolder: false // will copy "images" folder with all files inside
  }),
  eslint({})
]

Agora é possível definir como o ESBuild fará o build do projeto, quais os arquivos serão considerados entry points e que loaders serão utilizados para trabalharmos com diferentes tipos de arquivos.

//Código omitido
const { rebuild, stop } = await build({
  plugins,
  platform: 'browser',
  format: 'esm',
  // Bundles JavaScript.
  bundle: true,
  write: true,
  // Bundles JavaScript from (see `outfile`).
  entryPoints: ['src/main.js', 'src/assets/styles/main.css'],
  // Uses incremental compilation (see `chokidar.on`).
  incremental: true,
  // Bundles JavaScript to (see `entryPoints`).
  outdir: 'dist',
  treeShaking: !isDevMode(),
  sourcemap: isDevMode(),
  minify: !isDevMode(),
  target: isDevMode() ? ['esnext'] : ['es2018'],

  loader: {
    '.png': 'dataurl',
    '.jpg': 'file',
    '.jpeg': 'file',
    '.svg': 'text'
  }
})

Com todos os passos realizados até agora é possível definir como o servidor será iniciado, como os arquivos do projeto serão observados e como mudanças detectadas vão gerar um novo build.

Também definiremos como o servidor de desenvolvimento será notificado para que os arquivos sejam atualizados no browser após cada build novo.

 // `liveServer` local server for hot reload.
  if (!isDevMode()) {
    try {
      stop && stop()
    } finally {
      process.exit(0)
    }
  }


  // `chokidar` watcher source changes.
  chokidar
    // Watches TypeScript and React TypeScript.
    .watch('src/**/*.{js,ts,tsx}', {
      interval: 0 // No delay
    })
    // Rebuilds esbuild (incrementally -- see `build.incremental`).
    .on('all', () => {
      rebuild()
    })

  liveServer.start({
    // Opens the local server on start.
    open: false,
    // Uses `PORT=...` or 8080 as a fallback.
    port: 8080,
    //Host
    host: 'localhost',
    // Uses `public` as the local server folder.
    root: 'dist',
    middleware: [liveServerCors]
  })

  process.once('SIGTERM', async () => {
    try {
      stopWatch && stopWatch()
    } finally {
      process.exit(0)
    }
  })

O código completo desse sistema de build fica assim:

import { build } from 'esbuild'
import chokidar from 'chokidar'
import liveServer from 'live-server'

import path from 'path'
import aliasPlugin from 'esbuild-plugin-path-alias'
import eslint from 'esbuild-plugin-eslint'
import copy from 'esbuild-copy-files-plugin'

import { fileURLToPath } from 'url'
import { dirname } from 'path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

;(async () => {
  const isDevMode = () => {
    return process.env.NODE_ENV === 'development'
  }

  const liveServerCors = (req, res, next) => {
    res.setHeader('Access-control-allow-origin', '*')
    next()
  }

  const plugins = [
    aliasPlugin({
      '@/components': path.resolve(__dirname, './src/components'),
      '@/services': path.resolve(__dirname, './src/services'),
      '@/utils': path.resolve(__dirname, './src/utils'),
      '@/assets': path.resolve(__dirname, './src/assets')
    }),
    copy({
      source: ['./src/index.html', './src/config.modules.js'],
      target: 'dist',
      copyWithFolder: false // will copy "images" folder with all files inside
    }),
    eslint({})
  ]

  // `esbuild` bundler for JavaScript / TypeScript.
  const { rebuild, stop } = await build({
    plugins,
    platform: 'browser',
    format: 'esm',
    // Bundles JavaScript.
    bundle: true,
    write: true,
    // Bundles JavaScript from (see `outfile`).
    entryPoints: ['src/main.js', 'src/assets/styles/main.css'],
    // Uses incremental compilation (see `chokidar.on`).
    incremental: true,
    // Bundles JavaScript to (see `entryPoints`).
    outdir: 'dist',
    treeShaking: !isDevMode(),
    sourcemap: isDevMode(),
    minify: !isDevMode(),
    target: isDevMode() ? ['esnext'] : ['es2018'],

    loader: {
      '.png': 'dataurl',
      '.jpg': 'file',
      '.jpeg': 'file',
      '.svg': 'text'
    }
  })

  // `liveServer` local server for hot reload.
  if (!isDevMode()) {
    try {
      stop && stop()
    } finally {
      process.exit(0)
    }
  }


  // `chokidar` watcher source changes.
  chokidar
    // Watches TypeScript and React TypeScript.
    .watch('src/**/*.{js,ts,tsx}', {
      interval: 0 // No delay
    })
    // Rebuilds esbuild (incrementally -- see `build.incremental`).
    .on('all', () => {
      rebuild()
    })

  liveServer.start({
    // Opens the local server on start.
    open: false,
    // Uses `PORT=...` or 8080 as a fallback.
    port: 8080,
    //Host
    host: 'localhost',
    // Uses `public` as the local server folder.
    root: 'dist',
    middleware: [liveServerCors]
  })

  process.once('SIGTERM', async () => {
    try {
      stopWatch && stopWatch()
    } finally {
      process.exit(0)
    }
  })
})()

Para finalizar essas configurações, podemos incluir no package.json os scripts de build e inicialização do projeto e também as configurações padrões de estilo de lint.

//package.json
{
  "standard": {
    "ignore": [
      "/node_modules",
      "/dist",
      "/android",
      ".idea",
      "esbuild.config.js"
    ]
  },
  "lint-staged": {
    "*.js": [
      "standard",
      "prettier --write"
    ]
  },
  "scripts": {
    "lint": "standard --fix",
    "dev": "cross-env NODE_ENV=development node ./esbuild.config.js -w liverServer --cors",
    "build": "cross-env NODE_ENV=production node ./esbuild.config.js",
    "start": "pnpm run dev && pnpm run lint",
    "prod": "pnpm run build",
    "api": "json-server --watch api/db.json",
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}

As chaves list-staged e standard contém configurações relacionadas ao lint do projeto, enquanto a chave scripts, contém os scripts de inicialização e build do projeto.

O Arquivo package.json completo fica assim:

{
  "name": "app-hello",
  "version": "1.0.0",
  "description": "",
  "main": "./src/main.js",
  "type": "module",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "standard": {
    "ignore": [
      "/node_modules",
      "/dist",
      "/android",
      ".idea",
      "esbuild.config.js"
    ]
  },
  "lint-staged": {
    "*.js": [
      "standard",
      "prettier --write"
    ]
  },
  "scripts": {
    "lint": "standard --fix",
    "dev": "cross-env NODE_ENV=development node ./esbuild.config.js -w liverServer --cors",
    "build": "cross-env NODE_ENV=production node ./esbuild.config.js",
    "start": "pnpm run dev && pnpm run lint",
    "prod": "pnpm run build",
    "api": "json-server --watch api/db.json",
    "test": "echo \"Error: no test specified\" && exit 1"
  },  
  "devDependencies": {
    "chokidar": "^3.5.3",
    "cross-env": "^7.0.3",
    "esbuild": "^0.15.5",
    "esbuild-copy-files-plugin": "^1.1.0",
    "esbuild-plugin-eslint": "^0.1.1",
    "esbuild-plugin-path-alias": "^1.0.7",
    "esbuild-serve": "^1.0.1",
    "eslint": "^8.29.0",
    "eslint-plugin-fetch": "^0.0.1",
    "eslint-plugin-import": "^2.26.0",
    "http-server": "^14.1.1",
    "json-server": "^0.17.1",
    "lint-staged": "^13.1.0",
    "live-server": "^1.2.2",
    "shx": "^0.3.4",
    "standard": "^17.0.0",
    "watch": "^1.0.2"
  },
  "dependencies": {
    "htm": "^3.1.1",
    "terezzu": "^0.1.8"
  }
}

Observe que nas últimas linhas do package.json temos as dependências de produção. Essas dependências podem ser removidas e você pode adicionar novas de acordo com sua necessidade.

Eu utilizo essa mesma configuração nos meus projetos, e você pode conferir isso nos repositórios que compartilho abaixo.

Micro Front End

Template padrão para projetos SPA

Considerações finais

Essas configurações não são fixas, você pode adaptar todas para seus projetos, no entanto, as configurações disponibilizadas aqui são mais que suficientes para formatar toda a base de código de seus projetos.

Essas configurações visam garantir uma boa experiência e um padrão de estilo de código confiável para todos os devs em suas equipes.

Aprenda mais sobre

Carregando publicação patrocinada...