Criando um monorepo de Web Components
Published: 2021-11-22 19:51:11

Ao utilizar Web Components para a criação de elementos customizáveis, precisamos montar um ambiente de desenvolvimento que nos proporcione o compartilhamento entre esses componentes. Neste artigo, iremos montar um ambiente básico de compartilhamento entre componentes, que pode ser aplicado a Web Components ou a qualquer outra biblioteca com Javascript que você precise utilizar.

Para esse ambiente iremos usar três principais ferramentas:
- [Lerna](https://lerna.js.org/)
- [Rollup](https://rollupjs.org/guide/en/)
- [Typescript](https://www.typescriptlang.org/)

## Estrutura inicial com Lerna

O Lerna iremos utilizar para poder gerenciar os nossos pacotes, principalmente as suas dependências. 

Comece instalando o lerna no seu projeto:

```
npm install -D lerna
```

> Você pode instalar o lerna globalmente para poder estar executando os comandos do lerna sem precisar utilizar o npx.

Em seguida execute o seguinte comando para o lerna criar a estrutura inicial para o seu projeto.

```
npx lerna init
```

Gerando a seguinte estrutura:

```
.
├── node_modules
├── package-lock.json
├── lerna.json
├── package.json
└── packages

```

Vamos criar um componente utilizando a linha de comando do lerna.

```
lerna create @meumonorepo/button packages --description="componente button" --es-module
```

Criando a seguinte estrutura:

```
.
├── README.md
├── src
│   └── button.js
├── __tests__
│   └── button.test.js
└── package.json
```

Como vamos utilizar Typescript, vamos modificar a extensão do arquivo `button.js` para `button.ts`.

Com isso, vamos modificar o conteúdo interno do `button.ts` adicionando o nosso botão:

```typescript
export class Button extends HTMLElement {
  shadow: ShadowRoot;

  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: "open" });
  }

  connectedCallback(): void {
    this.render();
  }

  private render(): void {
    this.shadow.innerHTML = `
            <button>
                <slot></slot>
            </button>
        `;
  }
}

customElements.define("my-button", Button);

```

## Configuração Typescript

Agora podemos começar a configuração do Typescript.

```
npm i -D typescript
```

Vamos criar um arquivo `tsconfig.json` na raiz do projeto:

```json
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "allowUnreachableCode": false,
    "forceConsistentCasingInFileNames": true,
    "importHelpers": true,
    "moduleResolution": "node",
    "noEmitHelpers": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": false,
    "noUnusedParameters": true,
    "skipLibCheck": true,
    "inlineSources": true,
    "target": "es2017",
    "module": "es2015",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "esModuleInterop": true,
    "lib": ["es2017", "dom"]
  },
  "exclude": [
    "**/node_modules/**",
    "**/dist/**"
  ]
}

```

E dentro do diretório `packages/button` adicione o arquivo `tsconfig.json`:

```json
{
  "extends": "../../tsconfig.json",
  "include": ["./src"],
}

```

## Adicionando o Rollup

Agora vamos fazer a configuração do Rollup:

```
npm i -D rollup rollup-plugin-typescript2 rollup-plugin-terser
```

Dentro do `packages/button` crie um arquivo chamado `rollup.config.json` e adicione o seguinte conteúdo:

```typescript
import pkg from './package.json';
import typescript from 'rollup-plugin-typescript2';
import { terser } from 'rollup-plugin-terser';

const plugins = [
    typescript({ typescript: require('typescript') }), 
    terser()
];

const external = [
  ...Object.keys(pkg.dependencies || {}),
  ...Object.keys(pkg.peerDependencies || {}),
];


const input = 'src/button.ts';

export default {
  input,
  output: {
    file: pkg.module,
    format: 'esm',
    sourcemap: true,
  },
  plugins,
  external,
};
```

A opção `external`, serve para dizermos ao Rollup, para não resolver as dependências dentro do componente, mas mantê-las como dependências externas, ou seja, como módulos do tipo Node. A ferramenta `terser` seja para minificar o nosso código e assim otimizá-lo.

Agora vamos modificar o `package.json` para adicionar o script para o Rollup e também o nome do arquivo final gerado:

```diff
{
  "name": "@meumonorepo/button",
  "version": "0.0.0",
  "description": "> componente button",
  "homepage": "",
  "license": "ISC",
  "main": "dist/button.js",
- "module": "dist/button.module.js",
+ "module": "dist/button.js",
  "directories": {
    "lib": "dist",
    "test": "__tests__"
  },
  "files": [
    "dist"
  ],
  "publishConfig": {
    "access": "public"
  },
  "scripts": {
-   "test": "echo \"Error: run tests from root\" && exit 1"
+   "build": "npx rollup -c "
  }
}

```

## Ambiente de desenvolvimento com Live demo

Agora vamos criar um ambiente de desenvolvimento para poder visualizar o nosso componente. 

Vamos criar um novo diretório na raiz chamado demo. E vamos modificar o arquivo `lerna.json` para poder contar com esse novo diretório.

```diff
{
  "packages": [
    "packages/*",
+   "demo/*"
  ],
  "version": "0.0.0"
}
```

Agora vamos criar um novo pacote dentro de demo:

```
lerna create @meumonorepo/app demo --es-module
```

Agora vamos adicionar uma configuração Rollup para gerar o pacote final que iremos colocar dentro de uma página HTML, por isso, dentro do diretório `demo/app` crie o arquivo `rollup.config.js`:

```javascript
import { nodeResolve } from '@rollup/plugin-node-resolve';

import pkg from './package.json';

const input = 'src/app.js';

export default [
  {
    input,
    output: {
      file: pkg.module,
      format: 'esm',
      sourcemap: true,
    },
    plugins: [nodeResolve()]
  },
];
```

Para a nossa configuração funcionar, precisamos instalar o `@rollup/plugin-node-resolve` para poder resolver as dependências internas utilizando o [Algoritmo de resolução de módulos Node](https://nodejs.org/api/modules.html#modules_all_together):

```
npx lerna add @rollup/plugin-node-resolve --scope=@meumonorepo/app --dev
```

Também vamos adicionar o botão que criamos como uma dependência dentro do nosso demo:

```
npx lerna add @meumonorepo/button --scope=@meumonorepo/app
```

E adicionar o live-server para poder executar um pequeno servidor com o nosso Live Demo.

```
npx lerna add live-server --scope=@meumonorepo/app --dev
```

Agora vamos importar o nosso botão dentro `app.js`:

```javascript
import '@meumonorepo/button';
```

Agora vamos modificar o `package.json` para poder receber a configuração da geração do build:

```diff
{
  "name": "@meumonorepo/app",
  "version": "0.0.0",
  "description": "> TODO: description",
  "homepage": "",
  "license": "ISC",
  "main": "dist/app.js",
  "module": "dist/app.module.js",
  "directories": {
    "lib": "dist",
    "test": "__tests__"
  },
  "files": [
    "dist"
  ],
  "publishConfig": {
    "access": "public"
  },
  "scripts": {
-   "test": "echo \"Error: run tests from root\" && exit 1",
+   "build": "npx rollup -c",
+   "start": "live-server"
  },
  "devDependencies": {
    "@rollup/plugin-node-resolve": "^13.0.6"
  },
  "dependencies": {
    "@meumonorepo/button": "^0.0.0"
  }
}
```

Agora vamos criar um arquivo `index.html` dentro do pacote demo, para usarmos o componente. 

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>

    <my-button>Hello</my-button>
    
    <script src="./dist/app.module.js"></script>
</body>
</html>
```

Agora vamos gerar os pacotes com o comando:

```
npx lerna run build
```

> O `lerna run` é responsável por verificar no package.json de cada pacote o script "build" e executá-lo, sempre levando em consideração a árvore de dependências dos pacotes. 

Vamos executar o nosso live demo através do seguinte comando:

```
npx lerna run start
```

## Composição entre componentes

Agora iremos criar um componente para poder reutilizar o componente botão dentro dele. Por isso vamos criar o pacote `@meumonorepo/card`:

```
npx lerna create @meumonorepo/card packages --es-module
```

Vamos adicionar o botão como uma dependência do card:

```
 npx lerna add @meumonorepo/button --scope=@meumonorepo/card
```

Também precisaremos adicionar no card toda a configuração com Rollup e Typescript que fizemos no botão. Modificando o `package.json`:

```json
{
  "name": "@meumonorepo/card",
  "version": "0.0.0",
  "description": "> TODO: description",
  "homepage": "",
  "license": "ISC",
  "main": "dist/card.js",
  "module": "dist/card.js",
  "directories": {
    "lib": "dist",
    "test": "__tests__"
  },
  "files": [
    "dist"
  ],
  "publishConfig": {
    "access": "public"
  },
  "scripts": {
    "build": "npx rollup -c "
  },
  "dependencies": {
    "@meumonorepo/button": "^0.0.0"
  }
}
```

Adicionando o Rollup:

```javascript
import pkg from './package.json';
import typescript from 'rollup-plugin-typescript2';
import { terser } from 'rollup-plugin-terser';

const plugins = [
    typescript({ typescript: require('typescript') }), 
    terser()
];

const external = [
  ...Object.keys(pkg.dependencies || {}),
  ...Object.keys(pkg.peerDependencies || {}),
];


const input = 'src/card.ts';

export default {
  input,
  output: {
    file: pkg.module,
    format: 'esm',
    sourcemap: true,
  },
  plugins,
  external,
};
```

E adicionando o `tsconfig.json`:

```
{
  "extends": "../../tsconfig.json",
  "include": ["./src"]
}

```

Agora vamos adicionar o código do nosso componente card básico:

```typescript
import "@meumonorepo/button";

export class Card extends HTMLElement {
  shadow: ShadowRoot;

  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: "open" });
  }

  connectedCallback(): void {
    this.render();
  }

  private render(): void {
    const style = `
     div {
        display: inline-block;
        width: 250px;
        padding: 16px;
        border: solid 1px #939393;
        font-family: sans-serif
     }

    `;
    this.shadow.innerHTML = `
            <style>${style}</style>
              <div>
                  <slot></slot>

                    <hr>
                    <my-button>Saiba mais</my-button>
              </div>
          `;
  }
}

customElements.define("my-card", Card);
```

Para visualizarmos o nosso componente precisamos, adicionar o card no Live demo:


```
npx lerna add @meumonorepo/card --scope=@meumonorepo/app 
```

```diff
  import '@meumonorepo/button';
+ import '@meumonorepo/card';
```

E adicionar o componente card no `index.html`:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <my-card>
      <h3>My image</h3>
      <img src="https://picsum.photos/200/200" />
      <p>
        Lorem Ipsum is simply dummy text of the printing and typesetting
        industry. Lorem Ipsum has been the industry's standard dummy text ever
        since the 1500s, when an unknown printer took a galley of type and
        scrambled it to make a type specimen book.
      </p>
    </my-card>

    <script src="./dist/app.module.js"></script>
  </body>
</html>

```

Executar o build:

```
npx lerna run build
```

E subir o servidor local: 

```
npx lerna run start
```

Enfim pessoal é isso, espero que esse tutorial possa te auxiliar a criar o seu ambiente de desenvolvimento. 

[Projeto no Github](https://github.com/nicoleoliveira/monorepo-example)