Introdução
E ae dev, tudo bem com você?
Você já deve ter trabalhado com projetos onde as importações de arquivos e módulos eram (ou ficaram) cada vez mais aninhadas. Em um determinado momento você se perde a cada "ponto, ponto, barra, ponto, ponto, barra" (e ainda espera um pouquinho pra ver se o editor de texto ajuda a completar o caminho, pra onde você realmente quer ir (profundo, não?).
Seria muito mágico se houvesse uma maneira de alterar isso:
import { MyClass } from "../../../../deep/module";
Pra isso:
import { MyClass } from "@/deep/module";
Pois é, e TEM!
Bora pro post?
Ah! mas antes disso... Esse post faz parte de uma série de artigos "drops" que tenho aqui! Veja a lista:
- Drops #01: Corrigindo vulnerabilidades em dependências com Yarn! (ou quase)
- Drops #02: Como alterar o autor do commit depois do push
- Drops #03: Usando aliases para importação de módulos em TypeScript!
- Drops #04: Desmistificando ponteiros no Golang!
Iniciando o projeto
Vamos começar criando um projeto e inicializando o nosso package.json
:
mkdir ts-module-aliases && cd ts-module-aliases && yarn init -y
Em seguida, vamos adicionar algumas dependências de desenvolvimento, sendo elas:
- O TypeScript (duh!);
- O ts-node-dev (que será responsável por rodar o nosso código em modo de desenvolvimento);
- O Jest (precisaremos configurar algumas coisas no Jest para que ele interprete os caminhos absolutos que usaremos no nosso código);
- O tsconfig-paths (esse carinha será responsável por viabilizar o uso dos aliases).
- O Babel (ficará encarregado de realizar a construção do nosso projeto, interpretando os aliases e transpilando o código com os caminhos respectivos).
Execute o comando:
yarn add typescript@4.0.3 ts-node-dev@1.0.0 jest@26.6.0 ts-jest@26.4.1 @types/jest@26.0.15 tsconfig-paths@3.9.0 @babel/core@7.12.3 @babel/cli@7.12.1 @babel/node@7.12.1 @babel/preset-env@7.12.1 @babel/preset-typescript@7.12.1 babel-plugin-module-resolver@4.0.0 -D
Note que aqui foram fixadas as versões de cada dependência. Apenas para que a minha configuração e a sua sejam feitas com as mesmas versões dos pacotes.
Depois de instalar as dependências vamos iniciar as configurações!
Configurando o TypeScript
Na raiz do projeto, crie um arquivo tsconfig.json
com a seguinte configuração:
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"lib": ["es6", "dom"],
"allowJs": true,
"rootDir": ".",
"outDir": "./dist/lib",
"declarationDir": "./dist/@types",
"declaration": true,
"removeComments": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*", "**/*.spec.ts"]
}
Criando a base do projeto
Depois de configurar o Typescript, vamos criar alguns arquivos partindo da raiz do projeto, dentro da pasta src
:
src/services/BarService.ts
:
export class BarService {
bar() {
console.log(`Hi! I'm bar method from BarService :)`);
}
}
src/controllers/FooController.ts
:
import { BarService } from "../services/BarService";
export class FooController {
private readonly barService: BarService;
constructor() {
this.barService = new BarService();
}
foo() {
this.barService.bar();
}
}
src/index.ts
:
import { FooController } from "./controllers/FooController";
const fooController = new FooController();
fooController.foo();
Para finalizar, vamos adicionar o script no package.json
que executa o nosso código em modo de desenvolvimento:
{
"scripts": {
"dev": "ts-node-dev --no-notify src/index.ts"
}
}
Perceba que até aqui, ainda não temos um bom exemplo de arquivos SUPER aninhados. Será possível visualizar isso ao criarmos os nossos arquivos de testes!
Configurando o Jest
Na raiz do projeto, crie um arquivo jest.config.js
com a seguinte configuração:
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
clearMocks: true,
coverageDirectory: "__tests__/coverage",
coverageProvider: "v8",
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/__tests__/**/*.spec.ts"],
};
NOTA: os testes da nossa aplicação, ficarão na raiz do projeto, dentro da pasta
__tests__
.
Vamos então criar os nossos arquivos de testes. Partindo da raiz do projeto, dentro da pasta __tests__
:
__tests__/unit/controllers/FooController.spec.ts
:
import { FooController } from "../../../src/controllers/FooController";
import { BarService } from "../../../src/services/BarService";
describe("Unit: FooController", () => {
let fooController: FooController;
beforeEach(() => {
fooController = new FooController();
});
describe("foo", () => {
it("should call bar method from BarService", () => {
const spy = jest.spyOn(BarService.prototype, "bar").mockImplementation();
fooController.foo();
expect(spy).toBeCalled();
});
});
});
__tests__/unit/services/BarService.spec.ts
:
import { BarService } from "../../../src/services/BarService";
describe("Unit: BarService", () => {
let barService: BarService;
beforeEach(() => {
barService = new BarService();
});
describe("foo", () => {
it("should call console.log", () => {
const spy = jest.spyOn(console, "log").mockImplementation();
barService.bar();
expect(spy).toBeCalledWith(`Hi! I'm bar method from BarService :)`);
});
});
});
Olha ai, os benditos "ponto, ponto, barra, ponto, ponto, barra"!
Configurando os aliases no projeto
Vamos adicionar a configuração abaixo no tsconfig.json
:
{
"compilerOptions": {
// (...)
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
}
}
}
Esse mapeamento, fará com que todo @/*
seja um alias para ./src/*
(com o baseUrl
sendo a raiz do nosso projeto).
Vamos agora, fazer com que o ts-node-dev
seja capaz de entender os nossos aliases. Adicione no script de dev (no package.json
), O trecho -r tsconfig-paths/register
:
- "dev": "ts-node-dev --no-notify src/index.ts"
+ "dev": "ts-node-dev -r tsconfig-paths/register --no-notify src/index.ts"
A partir daqui, podemos alterar as importações! Altere isso:
import { FooController } from "../../../src/controllers/FooController";
import { BarService } from "../../../src/services/BarService";
Para isso:
import { FooController } from "@/controllers/FooController";
import { BarService } from "@/services/BarService";
NOTA: Altere todas as importações para usarmos o alias ao invés dos
../
ou./
.
Ao executarmos o comando yarn dev
, já podemos utilizar os aliases durante o desenvolvimento, porém, ao executarmos o yarn test
, o Jest ainda não é capaz de entender os caminhos que estamos utilizando...
Vamos adicionar a propriedade moduleNameMapper
no arquivo jest.config.js
e fazer a seguinte configuração:
const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { compilerOptions } = require("./tsconfig.json");
module.exports = {
// (...)
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: "<rootDir>",
}),
};
Com isso, já é possível utilizarmos os aliases nas nossas importações!
O problema
Até aqui, conseguimos configurar os aliases e utilizá-los tanto nos arquivos de testes quanto no source do projeto. Entretanto, precisamos ainda configurar o comando de construção do nosso projeto, somente assim ele estará pronto para publicarmos e utilizarmos no ambiente produtivo.
Vamos configurar o comando yarn build
para construir o nosso projeto, e o comando yarn start
para executar a o pacote construído.
Adicione os scripts no package.json
.
{
"scripts": {
// (...)
"build": "tsc",
"start": "node dist/lib/src/index.js"
}
}
A seguir, execute o comando:
yarn build && yarn start
Você perceberá que o projeto não pode ser executado, por conta do seguinte erro:
❯ yarn start
yarn run v1.22.5
$ node dist/lib/src/index.js
internal/modules/cjs/loader.js:968
throw err;
^
Error: Cannot find module '@/controllers/FooController'
Require stack:
Isso ocorre por que o tsc
não é capaz de entender esses aliases, inclusive, para nossa versão de produção, pouco importa se estamos usando aliases ou caminhos relativos, o que importa é que funcione!
Outro problema também, é que se notarmos os arquivos que foram construídos na nossa pasta dist
vamos encontrar todos os nossos arquivos de testes! O que não faz sentido algum ir pro ambiente de produção, não é mesmo?
Precisamos então:
- Fazer com que o comando de build gere um pacote funcional.
- Fazer com que o comando de build empacote apenas o código que irá para produção (e remover os arquivos de testes de lá).
Vamos fazer tudo isso com a substituição do tsc
pelo babel
!
NOTA: O babel será responsável por alterar os nossos aliases, para os respectivos caminhos no projeto
Configurando o Babel
Como já adicionamos as dependências do Babel no inicio do artigo, vamos começar o arquivo babel.config.js
na raiz do projeto, com a seguinte configuração:
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current",
},
},
],
"@babel/preset-typescript",
],
plugins: [
[
"module-resolver",
{
root: ["."],
alias: {
"^@/(.+)": "./src/\\1",
},
},
],
],
ignore: ["**/*.spec.ts"],
};
Com isso o babel irá alterar todo ^@/(.+)
para ./src/\\1
, e.g.: @/services/BarService
para ../services/BarService
.
Vamos agora criar o arquivo tsconfig.build.json
que irá herdar todas as configurações do nosso tsconfig.json
e será usado apenas para criar os arquivos de declaração de tipos do projeto (dentro da pasta dist/@types
). Isso é necessário, pois o Babel não fará esse trabalho. Adicione o seguinte no arquivo:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
},
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
}
Em seguida, altere o script start
(não precisaremos mais do src
ali) e o de build
.
Adicione também o script postbuild
no package.json
:
{
"scripts": {
"start": "node dist/lib/index.js",
"build": "babel src --extensions \".js,.ts\" --out-dir dist/lib --copy-files --no-copy-ignored",
"postbuild": "tsc -p tsconfig.build.json --emitDeclarationOnly"
}
}
Vamos apagar a pasta dist
gerada anteriormente, construir o projeto com o Babel, e então rodar o comando de produção:
rm -rf dist && yarn build && yarn start
yarn run v1.22.5
$ babel src --extensions ".js,.ts" --out-dir dist/lib --copy-files --no-copy-ignored
Successfully compiled 3 files with Babel (704ms).
$ tsc -p tsconfig.build.json --emitDeclarationOnly
✨ Done in 3.74s.
yarn run v1.22.5
$ node dist/lib/index.js
Hi! I'm bar method from BarService :)
✨ Done in 0.35s.
E Voilà!
Foi bastante coisa feita aqui! Espero que você tenha conseguido usar essa funcionalidade massa nos seus projetos, e entendido cada detalhe da configuração. No final desse artigo eu deixo o exemplo completo que desenvolvemos juntos!
Finalizando…
Bem, é isso, por hoje é só!
Quero agradecer a você que chegou até aqui, e queria lhe pedir também para encaminhar-me as suas dúvidas, comentários, críticas, correções ou sugestões sobre a postagem.
Deixe seu ❤️ se gostou ou um 🦄 se esse post te ajudou de alguma maneira! Não se esqueça de ver os posts anteriores e me siga para maaaais conteúdos.
Até!
Top comments (2)
Thank you so much! You was helped me a lot!
You're welcome Tadeu!