Criando um monorepo de Web Components

nicoleoliveira

Nicole Oliveira

Posted on November 22, 2021

Criando um monorepo de Web Components

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:

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Gerando a seguinte estrutura:

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

Enter fullscreen mode Exit fullscreen mode

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

lerna create @meumonorepo/button packages --description="componente button" --es-module
Enter fullscreen mode Exit fullscreen mode

Criando a seguinte estrutura:

.
├── README.md
├── src
│   └── button.js
├── __tests__
│   └── button.test.js
└── package.json
Enter fullscreen mode Exit fullscreen mode

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:

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);

Enter fullscreen mode Exit fullscreen mode

Configuração Typescript

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

npm i -D typescript
Enter fullscreen mode Exit fullscreen mode

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

{
  "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/**"
  ]
}

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

Adicionando o Rollup

Agora vamos fazer a configuração do Rollup:

npm i -D rollup rollup-plugin-typescript2 rollup-plugin-terser
Enter fullscreen mode Exit fullscreen mode

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

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,
};
Enter fullscreen mode Exit fullscreen mode

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:

{
  "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 "
  }
}

Enter fullscreen mode Exit fullscreen mode

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.

{
  "packages": [
    "packages/*",
+   "demo/*"
  ],
  "version": "0.0.0"
}
Enter fullscreen mode Exit fullscreen mode

Agora vamos criar um novo pacote dentro de demo:

lerna create @meumonorepo/app demo --es-module
Enter fullscreen mode Exit fullscreen mode

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:

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()]
  },
];
Enter fullscreen mode Exit fullscreen mode

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:

npx lerna add @rollup/plugin-node-resolve --scope=@meumonorepo/app --dev
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

import '@meumonorepo/button';
Enter fullscreen mode Exit fullscreen mode

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

{
  "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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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

<!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>
Enter fullscreen mode Exit fullscreen mode

Agora vamos gerar os pacotes com o comando:

npx lerna run build
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

 npx lerna add @meumonorepo/button --scope=@meumonorepo/card
Enter fullscreen mode Exit fullscreen mode

Também precisaremos adicionar no card toda a configuração com Rollup e Typescript que fizemos no botão. Modificando o package.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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Adicionando o Rollup:

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,
};
Enter fullscreen mode Exit fullscreen mode

E adicionando o tsconfig.json:

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

Enter fullscreen mode Exit fullscreen mode

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

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);
Enter fullscreen mode Exit fullscreen mode

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

npx lerna add @meumonorepo/card --scope=@meumonorepo/app 
Enter fullscreen mode Exit fullscreen mode
  import '@meumonorepo/button';
+ import '@meumonorepo/card';
Enter fullscreen mode Exit fullscreen mode

E adicionar o componente card no index.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>

Enter fullscreen mode Exit fullscreen mode

Executar o build:

npx lerna run build
Enter fullscreen mode Exit fullscreen mode

E subir o servidor local:

npx lerna run start
Enter fullscreen mode Exit fullscreen mode

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

Projeto no Github

💖 💪 🙅 🚩
nicoleoliveira
Nicole Oliveira

Posted on November 22, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related