TypeScript e Deno: módulos e permissões

1. Introdução ao Deno como Runtime TypeScript Nativo

Deno, criado por Ryan Dahl (mesmo criador do Node.js), surgiu como uma resposta às limitações arquiteturais do Node.js. Diferentemente do Node, que trata TypeScript como um "cidadão de segunda classe" exigindo transpilação prévia com tsc ou ts-node, o Deno possui suporte nativo a TypeScript. Isso significa que arquivos .ts são executados diretamente sem configuração adicional de compiladores.

Enquanto o Node.js gerencia dependências via node_modules e package.json, o Deno adota um modelo descentralizado baseado em URLs. Não há mais a necessidade de um diretório gigantesco de módulos — as dependências são baixadas e cacheadas sob demanda. Para projetos TypeScript modernos que buscam simplicidade e segurança, Deno representa uma alternativa atraente, eliminando a complexidade de configurações de build e oferecendo um runtime verdadeiramente seguro por padrão.

2. Sistema de Módulos no Deno: Importando com URLs

No Deno, a importação de módulos é feita diretamente via URLs. Isso elimina a necessidade de um registro centralizado como o npm:

// Importando um módulo de URL absoluta
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";

// Importando módulo local relativo
import { minhaFuncao } from "./utils/helpers.ts";

// Importando com alias para versionamento
import { concat } from "https://deno.land/x/concatenate@v1.0.0/mod.ts";

O versionamento é embutido na própria URL, garantindo reprodutibilidade. O Deno gerencia um cache global e um arquivo deno.lock que registra as versões exatas das dependências:

# Atualizar cache e lock file
deno cache --lock=deno.lock --lock-write src/main.ts

3. Módulos Padrão do Deno e Compatibilidade com npm

O Deno oferece uma biblioteca padrão (std/) rica e auditada, que cobre operações comuns:

import { copySync, ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts";
import { join, extname } from "https://deno.land/std@0.224.0/path/mod.ts";
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";

// Exemplo prático
const caminho = join("data", "arquivo.txt");
console.log(`Extensão: ${extname(caminho)}`);

ensureDir("./logs");
copySync("origem.txt", "destino.txt");

Além disso, o Deno suporta pacotes npm diretamente via npm: specifier:

// Importando pacote npm
import express from "npm:express@4.18.2";
import { v4 as uuidv4 } from "npm:uuid@9.0.0";

// Usando módulos built-in do Node para compatibilidade
import { readFileSync } from "node:fs";
import { createServer } from "node:http";

Isso permite uma transição gradual de projetos Node.js para Deno, mantendo dependências existentes.

4. O Sistema de Permissões do Deno

Deno adota uma abordagem de segurança radical: por padrão, nenhum acesso a arquivos, rede ou ambiente é permitido. O desenvolvedor deve explicitamente conceder permissões via flags:

// Este código falhará sem permissões adequadas
const conteudo = await Deno.readTextFile("./dados.json");
console.log(conteudo);

Para executar, é necessário:

# Conceder permissão de leitura
deno run --allow-read ./app.ts

# Múltiplas permissões
deno run --allow-read --allow-write --allow-net --allow-env ./app.ts

# Permissão granular para diretórios específicos
deno run --allow-read=/home/user/data --allow-write=/tmp ./app.ts

# Negar permissões específicas (útil em scripts com muitas permissões)
deno run --allow-read --allow-write --deny-net ./app.ts

As principais flags de permissão são:
- --allow-read: leitura de arquivos
- --allow-write: escrita de arquivos
- --allow-net: acesso à rede
- --allow-env: acesso a variáveis de ambiente
- --allow-run: execução de subprocessos
- --allow-sys: acesso a informações do sistema

5. Configuração de Permissões com deno.json e deno.jsonc

Para projetos maiores, gerenciar permissões via linha de comando torna-se impraticável. O Deno permite declarar permissões no arquivo de configuração:

// deno.jsonc
{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true
  },
  "tasks": {
    "start": "deno run --allow-net --allow-read=./public main.ts",
    "test": "deno test --allow-read --allow-net"
  },
  "lint": {
    "rules": {
      "exclude": ["no-explicit-any"]
    }
  }
}

Para permissões estáticas, use deno.json com a seção permissions:

{
  "permissions": {
    "read": true,
    "write": false,
    "net": ["deno.land", "api.exemplo.com"],
    "env": ["DB_HOST", "DB_PORT"]
  }
}

O import_map.json permite gerenciar módulos e permissões conjuntamente:

{
  "imports": {
    "std/": "https://deno.land/std@0.224.0/",
    "lodash": "npm:lodash@4.17.21"
  }
}

6. Exemplos Práticos: Aplicação TypeScript com Módulos e Permissões

Servidor HTTP com permissão de rede

import { serve } from "https://deno.land/std@0.224.0/http/server.ts";

const handler = (req: Request): Response => {
  const url = new URL(req.url);

  if (url.pathname === "/api/status") {
    return new Response(JSON.stringify({ status: "ok" }), {
      headers: { "Content-Type": "application/json" },
    });
  }

  return new Response("Hello TypeScript + Deno!", {
    headers: { "Content-Type": "text/plain" },
  });
};

console.log("Servidor rodando em http://localhost:3000");
await serve(handler, { port: 3000 });
deno run --allow-net server.ts

Leitura de arquivos com tratamento de erros

import { join } from "https://deno.land/std@0.224.0/path/mod.ts";

async function lerArquivoSeguro(caminho: string): Promise<string | null> {
  try {
    const dados = await Deno.readTextFile(caminho);
    return dados;
  } catch (erro) {
    if (erro instanceof Deno.errors.PermissionDenied) {
      console.error("Permissão negada para ler o arquivo");
      return null;
    }
    if (erro instanceof Deno.errors.NotFound) {
      console.error("Arquivo não encontrado");
      return null;
    }
    throw erro;
  }
}

const dados = await lerArquivoSeguro(join("data", "config.json"));
console.log(dados);
deno run --allow-read=./data leitor.ts

Combinação de módulos npm e Deno

import express from "npm:express@4.18.2";
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
import { v4 as uuid } from "npm:uuid@9.0.0";

const app = express();

app.get("/api/items", (_req, res) => {
  res.json([
    { id: uuid(), name: "Item 1" },
    { id: uuid(), name: "Item 2" },
  ]);
});

// Adaptando Express para o runtime Deno
const handler = (req: Request) => {
  // Lógica de adaptação...
  return new Response("Funcionando!");
};

await serve(handler, { port: 8080 });

7. Debugging e Testes com Deno e TypeScript

Deno oferece suporte nativo a testes com tipagem TypeScript:

// math.ts
export function soma(a: number, b: number): number {
  return a + b;
}

// math_test.ts
import { assertEquals } from "https://deno.land/std@0.224.0/testing/asserts.ts";
import { soma } from "./math.ts";

Deno.test("soma de dois números positivos", () => {
  assertEquals(soma(2, 3), 5);
});

Deno.test("soma com zero", () => {
  assertEquals(soma(0, 5), 5);
});
# Executar testes com permissões específicas
deno test --allow-read --allow-net

# Testes com cobertura
deno test --coverage=coverage

Para debugging no VS Code, configure o launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Deno",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "deno",
      "runtimeArgs": ["run", "--inspect-wait", "--allow-all"],
      "args": ["${file}"],
      "attachSimplePort": 9229
    }
  ]
}

Ferramentas nativas de lint e formatação:

# Formatação automática
deno fmt

# Linting
deno lint

# Verificação de tipos sem executar
deno check src/

8. Migração de Node.js para Deno: Desafios e Estratégias

Migrar projetos Node.js para Deno requer atenção a alguns pontos:

Adaptando imports CommonJS para ESM

// Antes (Node.js CommonJS)
const fs = require("fs");
const path = require("path");

// Depois (Deno ESM)
import * as fs from "node:fs";
import * as path from "node:path";

Gerenciando permissões ao migrar

// Script legado que precisa de várias permissões
async function migrarScript() {
  // Usar permissões granulares
  const config = JSON.parse(
    await Deno.readTextFile("./config.json")
  );

  // Substituir acesso direto a variáveis de ambiente
  const dbUrl = Deno.env.get("DATABASE_URL");

  // Usar fetch nativo em vez de axios
  const response = await fetch("https://api.exemplo.com");
}
deno run --allow-read=./config.json --allow-env=DATABASE_URL --allow-net=api.exemplo.com migrar.ts

Ferramentas de compatibilidade

# Compilar para executável único
deno compile --allow-read --allow-net app.ts

# Verificar compatibilidade com Node.js
deno check --compat app.ts

Para projetos complexos, considere usar o nodeCompat em deno.json:

{
  "compilerOptions": {
    "lib": ["deno.window"]
  },
  "unstable": ["node-compat"]
}

Referências