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/eject

v0.4.1

Published

eject · a definição JSON traduzida para o componente React que ela descreve — o MAPEAMENTO (o que `each` significa em React); quem imprime é o @tslite/format.

Readme

@loom-forge/eject

A definição JSON traduzida para o componente React que ela descreve. Dev/CI e tooling — nunca no caminho de render.

pnpm add -D @loom-forge/eject

Os @tslite/* que ele usa são 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+ os instalam sozinhos; uma faixa que não cruza vira aviso na instalação, e não uma segunda cópia.

import { ejectComponent } from "@loom-forge/eject";

const { code, notes } = ejectComponent(def);

O que é nosso, e o que não é

O printer não é daqui. Quem imprime é o @tslite/format, o emissor de FONTE do TSLite: contrato invertido do printer de execução — preserva tipo, preserva JSX, imprime comentário, quebra por largura. Aqui não há parser, precedência nem quebra de linha.

O que é nosso é o mapeamento: o que each significa em React, o que vira useState, de onde sai a key. Isso ninguém mais sabe.

Do @tslite/checker ele usa só a entry @tslite/checker/types — a álgebra de tipos, que é o que imprime a declaração de props; o walk do AST, que é o resto do checker, não entra.

| no JSON | no TSX | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | props declaradas | type XProps = { … } + o parâmetro anotado | | el: "div" | <div> · type: "Card" → <Card> | | props.children | o FILHO, nunca um atributo | | each + as | .map((item, index) => …) com key={item.id ?? index} | | when | {cond && …} — por item quando há laço | | $slot | uma ÁREA é uma prop de nó: {props.children} na default, {props.header} numa { name: "header" } | | state | useState por chave, com o setter | | updates[x].set | um useCallback que chama os setters — deps do que o set lê | | updates[x].params | o parâmetro da função, tipado | | effects[x] | useEffect, com on virando o array de dependências e cleanup no return | | events → update local | a chamada da função | | event nos params | o PARÂMETRO do handler: onChange={(event) => editar({ campo: "cpf", valor: event })} | | events → ação do host | invoke("nome", params) | | then / catch | async + const r = await invoke(...); !r.success roda o catch com r.error, !r.aborted o then com r.data | | pending["x/y"] | um useState de pendentes, ligado e desligado em volta do await | | apelido de commands | invoke("id", { ...ligados, ...passados }) — sai resolvido | | texto com {{ }} no meio | template literal |

A key repete a identidade do motor (item.id ?? índice), e não é escolha de estilo: é o que faz o TSX e a árvore resolvida concordarem sobre qual nó é qual.

Cada ÁREA do corpo vira uma prop de nó, que é como o React já as escreve. Um layout que abre regiões sai com uma prop por região, e o tipo das props as declara como ReactNode opcional:

type ShellProps = { header?: ReactNode; children?: ReactNode };
export const Shell = (props: ShellProps) => (
  <div>
    {props.header}
    {props.children}
  </div>
);

A default se chama children porque é o nome que o React lhe dá; uma nomeada ({ type: "$slot", props: { name: "header" } }) sai com o nome que o autor escreveu. Quem preenche cada área é o call-site, pelo campo slot do filho — e isso não aparece aqui, porque no TSX quem compõe é quem escreve o JSX.

O update é um useCallback, e as dependências saem do que o set LÊ — a regra do exhaustive-deps, derivada da expressão (o freeVars do @tslite/behavior), não escrita à mão:

const alternar = useCallback(() => {
  setAberto((aberto) => !aberto); // escreve o que lê → functional update, e a chave sai das deps
}, []);
const aplicar = useCallback(() => {
  setTotal(count * props.fator); // lê OUTRA chave e um membro de props → os dois nas deps
}, [count, props.fator]);

O setter é estável e params é argumento: nenhum dos dois entra. O functional update é a forma que não perde um disparo quando dois entram no mesmo batch — e é a que o React Compiler produziria. O useEffect continua com o on do documento como dependências: é o contrato do modelo, não o do lint.

Uma intenção é uma promise chain, e o TSX lê como uma (ADR-018 do repo). O que a intenção RECEBE é a raiz event dos params — o valor que o widget emitiu, o data da ação anterior num then, o error num catch — e o autor escreveu o mapeamento; o handler emitido é a tradução literal dele:

onChange={(event) => editar({ campo: "cpf", valor: event })}

onClick={async () => {
  iniciar();
  setPending((p) => ({ ...p, "corretor/validar-email": true }));
  const r = await invoke("corretor/validar-email", { email: props.email });
  setPending((p) => ({ ...p, "corretor/validar-email": false }));
  if (!r.success) falhou({ motivo: r.error.code });
  else if (!r.aborted) marcar({ valido: r.data.valido });
}}

O then de um update local roda depois do set, com event = undefined — é o iniciar() seguido do invoke acima. Um fluxo cancelado (aborted com sucesso) não roda nem then nem catch, e é o !r.aborted que o diz. Uma intenção que não lê event sai como () => …, e uma que não espera nada continua síncrona. O pending só existe num componente vivo — é o boundary que o guarda no motor, e aqui é um useState do próprio componente, declarado quando alguma expressão o lê; a partir daí todo despacho ao host o liga e desliga, como o motor faz. Dentro de um useEffect, cuja função não pode ser async, a cadeia que espera roda numa IIFE (void (async () => { … })()).


Ele consome a definição COMPILADA

A fase de compilação (@loom-forge/tslite/compile) já respondeu onde uma ilha começa e termina, e já decidiu que texto interpolado vira template literal. Segmentar de novo aqui seria a terceira resposta para a mesma pergunta.

As expressões de um update e de um efeito não moram na árvore, e quem as alcança é o compileDefinition do @loom-forge/forge — o mesmo que o runtime usa. O eject não tem caminho próprio de compilação: ele recebe a definição compilada e traduz.

O que cada intenção EXECUTA também não é pergunta daqui: vem da rota do forge (settledRoute), a mesma que o grafo e o check usam. Passe a montagem do sistema para que o eject saiba o que o documento sozinho não decide:

ejectComponent(def, { mounting: mountingOf(sys.components.values()) });

Sem ela, o documento é o sistema: o que ele declara sai resolvido, e o resto sai como id.


O que a tradução NÃO alcança sai em notes

Nunca em silêncio. Hoje:

  • placement e layout — quem posiciona é o renderer do layout; o TSX mostra os filhos, não o arranjo que o planner calcula. O caminho é um emitter pareado por nome, ao lado do renderer;
  • provides — seria um Provider ao redor;
  • o vocabulário de comando — o que o documento resolve sai resolvido; um nome que depende de onde o componente monta (o $slot de uma sessão, um vocabulário de outro componente) sai como nome, e no React pediria um vocabulário de comando do host. Quem declara commands ganha a nota pelo mesmo motivo;
  • nome livre que venha da stdlib ou do host (length, map) — no TSX precisaria de um import que o JSON não declara;
  • um nó com props.children E filhos na árvore — no motor a árvore sobrescreve a prop, em silêncio;
  • um campo que não compila — ele ficou com o texto que o autor escreveu, e sai assim no TSX, como string. A nota traz o endereço e o código do diagnóstico da compilação;
  • os imports das peças — <Card> sai sem import, porque de qual módulo ele vem é conhecimento do host. O seam previsto é um resolvedor (descriptor.meta ou um moduleOf injetado), como o propsOf do check.

[!WARNING] O TSX gerado não é o que o palco renderiza. O motor resolve por bind → expand → resolve; este package traduz. São dois caminhos, e sem um teste de paridade que execute o ejetado e compare com o render, eles divergem em silêncio. O teste existe como plano, não como código — ver o TODO.md do root.


Porta de mão única

O cabeçalho do arquivo emitido diz isso, e é contrato: editar o TSX não volta para o JSON. {xs.map(f)} até levantaria para um each, mas {fazAlgo(xs)} não levanta para nada — a volta não existe em geral, e inventá-la poria código na árvore.