npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@loom-forge/forge

v0.4.0

Published

forge · o CONSTRUTOR de componentes (headless): declara componentes novos em JSON — IR, grafo, regras, slots, estado local e a expansão sobre o compositor. Zero dependências.

Readme

@loom-forge/forge

forge · o CONSTRUTOR de componentes, headless. O loom compõe componentes que já existem; o forge declara componentes novos em JSON — com props tipadas, estado local, slots com contrato e um grafo de dependência entre definições. Sem React e sem linguagem de expressão: o binder entra pelo port.

pnpm add @loom-forge/forge

O @tslite/graph é peer (ADR-021): a aplicação fornece uma instância do TSLite, a mesma para todo repo que a compõe. npm 7+ e pnpm 8+ o instalam sozinhos.

{
  "component": "UserCard",
  "props": { "id": "string", "titulo?": "string" },
  "state": { "aberto": { "type": "boolean", "init": false } },
  "updates": { "alternar": { "set": { "aberto": "{{ !state.aberto }}" } } },
  // um update pode declarar os ARGUMENTOS que aceita: `params: { n: "number" }`
  "body": {
    "id": "card",
    "type": "Card",
    "props": { "title": "{{ props.titulo }}" },
    "events": { "onClick": { "action": "alternar" } },
    "children": [{ "id": "slot", "type": "$slot" }],
  },
}

Um componente é uma função — exatamente como uma action é uma função. É desse isomorfismo que sai o resto: entry × privado, puro × sujo, requires/unable, grafo de dependência, propagação de tags. Ver docs/DESIGN.md.


As camadas

| camada | o quê | arquivo | | -------------- | ------------------------------------------------------------------ | ---------------------------- | | definição | ComponentDef + o registry (createComponentDefRegistry) | ir.ts | | compilação | a definição COMPILADA: texto → ilha, na árvore e fora dela | compile.ts | | análise | IR + grafo + regras + slots — o relógio LENTO, data-independente | ir/graph/rules/slots | | expansão | o seam sobre o compositor: custom → árvore pura, no relógio RÁPIDO | expand.ts |

A definição compilada

import {
  compileDefinition,
  compileNodeOf,
  componentSlotsOf,
} from "@loom-forge/forge";

// pelo PORT do binder (runtime): só a forma
const { def: compilada } = compileDefinition(userCard, compileNodeOf(binder));
// pela LINGUAGEM (autoria): a forma, os diagnósticos e as ilhas em memória
const { def, diagnostics, islands } = compileDefinition(userCard, compile);

Uma definição tem expressão fora da árvore: o provides, os params de um alias de commands, o init do estado, o set de um update, os params de um efeito e do cleanup — e os de cada elo then/catch da cadeia deles (ADR-018). O compile() da linguagem recebe um nó e só alcança a árvore — então cada consumidor compilava o resto por conta própria, com um nó-fantasma escrito na mão: um no applyUpdate, um no EffectRunner, um no eject. Três cópias da mesma pergunta.

componentSlotsOf(def) responde onde eles estão, e o caminho é o endereço (updates.somar.set, effects.carregar.then.params, effects.carregar.cleanup.params). A tabela COMPONENT_FIELDS classifica todo campo da definição e é exaustiva por keyof ComponentDef: campo novo não compila sem ser classificado.

A divisão de conhecimento não muda: o forge diz ONDE a expressão mora e a linguagem entra como função (CompileNode) — nenhum dos dois importa o outro. O tradutor devolve a forma e, quando tem o que dizer, os diagnósticos e as ilhas do nó; o forge os reancora no caminho do slot (updates.somar.set.n, state.n.init), e é assim que um erro de sintaxe num set chega à autoria com endereço. O port do binder fala só DefNode, e compileNodeOf(binder) é o tradutor estável dele — a memória do compileDefinition é por definição e por tradutor, então uma arrow nova por render nunca a acertaria.

[!WARNING] O runtime assume a definição COMPILADA. O applyUpdate e o bindPayload só ligam; quem compila é o defineComponent, uma vez, na entrada do registry. Um set que chegue cru vira o próprio texto no estado, porque o binder trata string como dado (ADR-111).

Definição e IR

import {
  createComponentDefRegistry,
  compileComponent,
  buildSystemIR,
} from "@loom-forge/forge";

const defs = createComponentDefRegistry().register(userCard);
const ir = compileComponent(userCard, { components: defs.names() });
const system = buildSystemIR([ir]); // grafo: outgoing/incoming, tags, ciclos, missing-deps

O IR é derivado, nunca editado à mão, nunca persistido como verdade — recomputado do source. E ele carrega só a árvore normalizada: o que se calcula sobre ela (arestas, deps, tags, ciclos) é artefato lateral — deriveFacts / factsOf / nodeFactsOf —, com as camadas transitivas opt-in. O binder deriva, o checker julga. A unidade do grafo é a definição (UserCard), não o nó-instância; as arestas são induzidas por nós dentro da árvore (type custom → uses; uma intenção → actions, com o id que ela EXECUTA — ver Comando). Containment não é dependência. Ver docs/IR.md.

Grafo

O motor é o @tslite/graph — ponto-fixo monótono por worklist, Tarjan iterativo, zero dependências. Não o reexportamos: quem precisa das primitivas importa de lá, direto. O que o buildSystemIR faz é montar o grafo do DOMÍNIO (quais são os nós, quais arestas contam) e instanciar os reticulados:

| fato | de onde vem | o quê | | ---------------- | --------------------------- | --------------------------------------------- | | facts.tags | propagateUnion | tags efetivas/transitivas, canônicas | | facts.incoming | invertEdges | "quem usa isto" — por onde o dirty sobe | | facts.cycles | detectCycles | componentes fortemente conexos | | computeUnable | reachable (em rules.ts) | availability transitivo (o Razor das Actions) |

A Linguagem não tem ciclo — o call graph dela é um DAG —, mas o grafo de quem a consome tem: um componente compõe outro que o compõe de volta. Sobre esse grafo não existe ordem topológica, existe ponto-fixo; e essa peça, que todo consumidor não-trivial acabava copiando, desceu para o TSLite. Este package copiava uma versão quadrática dela (1000 nós em 58 ms, 8000 em 4,7 s), e a cópia morreu quando o dono publicou a peça.

Regras e slots

import {
  checkSlots,
  checkCategoryPolicy,
  computeUnable,
  checkForbiddenTags,
  checkIdentity,
} from "@loom-forge/forge";

checkIdentity acusa um id repetido no documento de um componente (duplicate-id, com os endereços). O id é a identidade do nó — é por ele que o editor muta, que o patch acha o alvo e que a tela liga o foco ao documento —, e dois nós com o mesmo id fazem os três apontarem para o primeiro, sem erro nenhum. Componentes diferentes repetem id à vontade: a expansão os separa por instância.

checkSlots é o que o React não tem: o pai declara o contrato do que aceita como children (categoria, tags, leaf, cardinalidade). Composição ≠ dependência — A não conhece B, só o contrato, então isso NÃO cria aresta A→B, e a checagem mora no compositor (quem pôs B dentro de A).

O contrato é por área (slots é nome → SlotContract, e children é a default): cada região julga o grupo dela, e um filho que aponta para uma área que o pai não declara é slot-unknown — o erro mais barato de pegar aqui, porque em runtime ele viraria uma prop que o componente ignora e sumiria da tela. Um pai que não declara contrato nenhum não julga nada: não saber não é saber que não.

O computeUnable aceita a porta de ação e pergunta a ela, em vez de exigir a lista de indisponíveis pronta:

computeUnable(sys, { port });
// ação indisponível → quem a invoca é fonte, e todo dependente herda `unable`

Ele pergunta só pelo que o grafo de fato invoca — o eixo actions tem só o que SAI, porque update local é chamada interna (ADR-014), e tem o id RESOLVIDO: um apelido de sessão nunca é perguntado. Porta sem isAvailable não torna nada unable: não saber não é saber que não. E é um RETRATO: disponibilidade muda em runtime, e isto é o relógio lento — quem quiser acompanhar, recomputa.

Expansão

import { resolveComponentTree } from "@loom-forge/forge";

resolveComponentTree(node, { registry, binder, componentDefs: defs }, data);
// bind (dados) → expand (custom → árvore pura) → resolveTree (compositor)

A expansão é um pré-passo, e é isso que mantém o compositor puro: o resolveTree do core não sabe o que é um ComponentDef. Cada nó de tipo custom vira o corpo do componente ligado contra as props, com ids namespaced por instância; os children do call-site são injetados onde o corpo declara { "type": "$slot" } — resolvidos no escopo do COMPOSITOR, opacos para o componente.

Um corpo pode abrir várias ÁREAS, que é o que um layout de verdade pede (header, menu, main, rodapé): o corpo declara { "type": "$slot", "props": { "name": "header" } } e o filho do call-site diz em qual entra com slot — o irmão do placement, e os dois são campos do filho que falam do pai (um diz onde no layout, o outro em que área). Sem slot, o filho cai na área default, que é a composição de sempre:

{
  "type": "AppShell",
  "children": [
    { "id": "topo", "type": "AppHeader", "slot": "header" },
    { "id": "corpo", "type": "HomePage" },
  ],
}

Quem resolve depende de quem é o pai: um ComponentDef, aqui na expansão; uma peça registrada com regiões, na view — o grupo vira a prop de mesmo nome, e só o corpo vai ao planner. Nó nunca entra em props: duas posições de árvore seriam duas verdades sobre quem ganha.

Um componente vivo também recebe áreas: o marcador $stateful leva os children do call-site, e o boundary os repassa pelo children do resolveComponentTree. Sem isso, um layout com estado perderia os filhos que o compositor lhe deu — e perderia em silêncio.

Ela roda sempre, mesmo sem componentDefs: é também onde as diretivas de escopo (provides, commands) são consumidas. bodyOf(def) é o corpo com o açúcar do componente — o provides e o commands dele juntam com os da raiz, e a raiz ganha no mesmo nome —, e é o que o boundary de um componente vivo expande, com updates dizendo quais nomes resolvem ali dentro.

Cada nó sai com a origem (origins): o componente e o id que o autor deu, do mais de fora para o mais de dentro. O bodyOf carimba antes do bind, então a cópia de um each preserva o id autorado; a raiz de um corpo responde também pela chamada que ocupa; e a árvore do host não tem origem. É o que liga a tela de volta ao documento (ADR-017 do repo).

Contexto

import {
  deriveContext,
  checkContexts,
  requiredContextsOf,
} from "@loom-forge/forge";

const ctx = deriveContext(sys.components.values());
checkContexts(sys, ctx, { ambient: ["@acme/auth"] });

requiredContextsOf(defs); // componente → os nomes que ele exige de FORA

uses não é atributo, é dívida: ela sobe pelo grafo até alguém prover, e quem exige sem declarar está mentindo — erro, no nó por onde escapou. Qualquer nó provê (provides), o ComponentDef.provides é açúcar para a raiz do corpo, e o contrato tem de ser dado serializável. O que está provido num ponto é o que ele enxerga depois de MONTADO: um filho de slot enxerga o que o chamado provê no $slot. Ver docs/CONTEXT.md.

Do runtime, a mesma lista. O requiredContextsOf(defs) devolve, por componente, os nomes que ele exige de fora — o uses dele mais o que escapou de quem ele compõe, pelo mesmo ponto-fixo. É o que a view usa como SELETOR: uma seção viva pede ao host só esses nomes, e mudar um contexto fora da lista não a alcança (ADR-023 do repo). Uma pergunta, dois consumidores — a análise julga a borda com ela, e o runtime observa por ela.

E é por isso que as duas origens de contexto viajam separadas na expansão: ExpandOptions.contexts são os valores de AMBIENTE (o que o host provê), e ExpandOptions.installed é o que o DOCUMENTO instalou no ponto em que o corpo é expandido — o que o boundary de um componente vivo continua de onde a expansão parou.

Comando

{
  "component": "FormSection",
  "commands": {
    "submeter": { "action": "form/submit", "params": { "canal": "email" } },
    "limpar": "form/clear",
  },
  "body": {
    "id": "sec",
    "type": "Section",
    "children": [{ "id": "slot", "type": "$slot" }],
  },
}

Um nó declara o vocabulário da subárvore — nome local → ação. Um botão dispara submeter e não sabe o que isso executa: quem decide é o lugar onde ele monta. A resolução tem três degraus, update local → vocabulário mais próximo → porta raiz, e segue as regras do provides: o nó instala para baixo, o corpo do chamado é a subárvore do nó de chamada, e o filho de slot enxerga o vocabulário do $slot onde monta. A expansão consome a diretiva, então a view recebe o id. Os params do alias são aplicação parcial: entram por baixo dos do disparo. E o efeito resolve pelo vocabulário de onde o componente montou.

A mesma pergunta tem resposta sem rodar:

import { mountingOf, settledRoute } from "@loom-forge/forge";

buildSystemIR(irs, { commands: host }); // o eixo `actions` já sai com o id resolvido
const mounting = mountingOf(sys.components.values(), { host }); // o que cada um instala no slot

Um nome que nenhum vocabulário do sistema declara só pode chegar à porta, e é aresta de quem o escreveu. Um que algum vocabulário declara depende de onde o componente monta: ele escapa, como o uses de um contexto, e vira aresta de quem o resolve — na borda, é o vocabulário do host (commands) que responde. routeIntent/settledRoute são a resposta para UMA intenção, e é delas que o check e o eject perguntam. Ver o ADR-015 do repo.

Efeito

import { checkEffects } from "@loom-forge/forge";

Um efeito é um events-out cujo gatilho é uma mudança, não um gesto: mesma action, mesmos params, mesma aresta no grafo, mesma tipagem — e a mesma cadeia: then/catch valem num efeito (e no cleanup dele), e é assim que "carregar no mount" guarda o que carregou (ADR-018 do repo). O event do efeito em si é undefined; o do then, o que a ação devolveu. Declarar efeito torna o componente vivo, e infere a tag effectful, que propaga. E o laço — observar o que a própria ação escreve — se prova sem rodar, porque os dois lados são declarados. Ver docs/STATE.md §7.

Estado local

import {
  isLive,
  initState,
  applyUpdate,
  STATEFUL_TYPE,
} from "@loom-forge/forge";

Componente puro (sem updates nem effects) expande estaticamente. Componente vivo não expande: emite o marcador $stateful (dado, headless) e quem o materializa é a view — @loom-forge/forge-react. O estado é sempre local à instância; 100 <Counter/> são 100 escopos isolados. Nunca global. Ver docs/STATE.md.


Os vizinhos

O forge depende do compositor, nunca o contrário. Ele não conhece React (a view é o @loom-forge/forge-react), não conhece a linguagem de expressão (o binder entra pelo port BindingEngine, implementado pelo @loom-forge/tslite) e não conhece a camada de tipos (que é o @loom-forge/check, e depende DELE, não o contrário).