Publicando uma biblioteca TypeScript no npm

1. Preparação do Projeto

A estrutura de diretórios é fundamental para manter a organização e facilitar a manutenção. Recomenda-se a seguinte estrutura:

minha-biblioteca/
├── src/
│   └── index.ts
├── dist/
├── tests/
│   └── index.test.ts
├── package.json
├── tsconfig.json
├── .gitignore
└── README.md

O package.json deve conter as configurações essenciais:

{
  "name": "@seu-usuario/minha-biblioteca",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "test": "jest",
    "prepublishOnly": "npm test && npm run build"
  }
}

O .gitignore para projetos TypeScript deve incluir:

node_modules/
dist/
*.tsbuildinfo
coverage/
.env

2. Configuração do TypeScript Compiler

O tsconfig.json é o coração da configuração. Para bibliotecas, recomenda-se:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "tests"]
}

Para suporte dual (CommonJS e ESM), é necessário configurar dois arquivos de saída:

// tsconfig.cjs.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "./dist/cjs"
  }
}

// tsconfig.esm.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "esnext",
    "outDir": "./dist/esm"
  }
}

3. Empacotamento e Build

Os scripts de build devem automatizar o processo completo:

{
  "scripts": {
    "clean": "rm -rf dist",
    "build:cjs": "tsc -p tsconfig.cjs.json",
    "build:esm": "tsc -p tsconfig.esm.json",
    "build": "npm run clean && npm run build:cjs && npm run build:esm",
    "prepublishOnly": "npm run build && npm test"
  }
}

Exemplo de código fonte para a biblioteca:

// src/index.ts
export interface SaudacaoOptions {
  nome?: string;
  idioma?: 'pt' | 'en' | 'es';
}

export function saudacao(options: SaudacaoOptions = {}): string {
  const { nome = 'Mundo', idioma = 'pt' } = options;

  const saudacoes = {
    pt: `Olá, ${nome}!`,
    en: `Hello, ${nome}!`,
    es: `¡Hola, ${nome}!`
  };

  return saudacoes[idioma];
}

export function somar(a: number, b: number): number {
  return a + b;
}

4. Gerenciamento de Dependências e Peer Dependencies

É crucial entender a diferença entre os tipos de dependências:

{
  "dependencies": {
    "lodash": "^4.17.21"  // Dependência de runtime
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "jest": "^29.0.0",
    "@types/jest": "^29.0.0",
    "@types/lodash": "^4.14.0"
  },
  "peerDependencies": {
    "react": "^18.0.0"  // Para bibliotecas React
  },
  "peerDependenciesMeta": {
    "react": {
      "optional": true
    }
  }
}

Boas práticas:
- dependencies: Apenas o necessário para a biblioteca funcionar
- devDependencies: Ferramentas de desenvolvimento e tipos
- peerDependencies: Bibliotecas que o usuário já deve ter instalado

5. Publicação no npm

Processo completo de publicação:

# Login no npm
npm login

# Versionamento semântico
npm version patch  # 1.0.0 -> 1.0.1
npm version minor  # 1.0.0 -> 1.1.0
npm version major  # 1.0.0 -> 2.0.0

# Publicação
npm publish --access public

Para pacotes scoped (privados por padrão):

{
  "name": "@seu-usuario/minha-biblioteca",
  "publishConfig": {
    "access": "public"
  }
}

Publicação com tags:

npm publish --tag beta
npm dist-tag add @seu-usuario/minha-biblioteca@1.0.0 latest

6. Testes e Qualidade Antes da Publicação

Configuração completa de testes com Jest:

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/tests'],
  testMatch: ['**/*.test.ts'],
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

Exemplo de teste:

// tests/index.test.ts
import { saudacao, somar } from '../src';

describe('saudacao', () => {
  it('deve retornar saudação padrão', () => {
    expect(saudacao()).toBe('Olá, Mundo!');
  });

  it('deve retornar saudação personalizada', () => {
    expect(saudacao({ nome: 'João' })).toBe('Olá, João!');
  });

  it('deve suportar diferentes idiomas', () => {
    expect(saudacao({ nome: 'John', idioma: 'en' })).toBe('Hello, John!');
  });
});

describe('somar', () => {
  it('deve somar dois números corretamente', () => {
    expect(somar(2, 3)).toBe(5);
  });
});

Configuração de linting:

// .eslintrc.json
{
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "rules": {
    "@typescript-eslint/explicit-function-return-type": "error",
    "@typescript-eslint/no-explicit-any": "error"
  }
}

7. Documentação e Exemplos

Geração automática de documentação com TypeDoc:

npm install --save-dev typedoc
// typedoc.json
{
  "entryPoints": ["src/index.ts"],
  "out": "docs",
  "excludePrivate": true,
  "excludeProtected": true,
  "theme": "default"
}

Exemplo de README completo:

# Minha Biblioteca

[![npm version](https://badge.fury.io/js/@seu-usuario/minha-biblioteca.svg)](https://badge.fury.io/js/@seu-usuario/minha-biblioteca)
[![Build Status](https://travis-ci.org/seu-usuario/minha-biblioteca.svg)](https://travis-ci.org/seu-usuario/minha-biblioteca)
[![Coverage Status](https://coveralls.io/repos/github/seu-usuario/minha-biblioteca/badge.svg)](https://coveralls.io/github/seu-usuario/minha-biblioteca)

## Instalação

```bash
npm install @seu-usuario/minha-biblioteca

Uso

import { saudacao, somar } from '@seu-usuario/minha-biblioteca';

// Saudação personalizada
console.log(saudacao({ nome: 'Maria', idioma: 'pt' }));
// Output: Olá, Maria!

// Operações matemáticas
console.log(somar(10, 20));
// Output: 30

API

saudacao(options?)

Retorna uma saudação personalizada.

Parâmetros:
- options.nome (string, opcional): Nome da pessoa (padrão: 'Mundo')
- options.idioma ('pt' | 'en' | 'es', opcional): Idioma da saudação (padrão: 'pt')

Retorno: string

somar(a, b)

Soma dois números.

Parâmetros:
- a (number): Primeiro número
- b (number): Segundo número

Retorno: number
```

Referências