Criando um monorepo de Web Components
Nicole Oliveira
Posted on November 22, 2021
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
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:
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:
{
"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
:
{
"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:
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:
{
"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.
{
"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
:
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:
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
:
import '@meumonorepo/button';
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"
}
}
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>
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
:
{
"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:
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:
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
import '@meumonorepo/button';
+ import '@meumonorepo/card';
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>
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.
Posted on November 22, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.