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

v0.4.0

Published

loom · o adapter de BIND via TSLite: compile() (expressão em texto → ilha #expr) e o binder que a executa, gateado por CapabilityProfile, eval-free, com a stdlib do @tslite/std no escopo.

Readme

@loom-forge/tslite

loom · a linguagem de expressão da definição. Duas fases, e elas são dois relógios: a compilação traduz o texto autorado ("{{ props.nome }}") numa ilha #expr, e o bind executa o que ela produziu contra um escopo. Implementa o port BindingEngine do @loom-forge/core.

pnpm add @loom-forge/tslite

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 { compile, createTsliteBinder } from "@loom-forge/tslite";

const { def, diagnostics } = compile(autorada); // texto → ilha #expr. NÃO avalia.
const engine = createEngine({
  binder: createTsliteBinder({ globals: { upper } }),
});

Quem só consolida (um servidor que compila a definição no save) importa o subpath, que não carrega o check nem a stdlib de runtime — só o que interpreta a string e a lista do que o deploy recusaria:

import { compile } from "@loom-forge/tslite/compile";

compile(def) — a fase de compilação

compile(root: DefNode, opts?: { profile?: CapabilityProfile }):
  { def: DefNode; diagnostics: CompileDiagnostic[]; islands: CompiledIsland[] }

A definição autorada carrega a expressão como texto e é o SoT — é o que o autor edita, o canvas desenha e o editor diffa. A compilada carrega o AST como ilha #expr no lugar, e é derivada. A forma é a mesma dos dois lados, então o endereço (#0.1.props.title) vale para ambos e não há source map.

  • não avalia — não tem dados e não computa valor;
  • não lança — o campo que não compila fica com o texto original e sai um diagnóstico, com o endereço do campo, o caminho como lista (path, índice e chave distintos), o texto do campo (source) e o trecho culpado nele (span). Os códigos são os do TSLite: parse-error · lex-error · island-not-a-value · unserializable-expression · node-not-allowed · forbidden-identifier · dynamic-computed; e o nosso missing-binding;
  • quem interpreta a string é o parseValue do @tslite/template, com o directives que o slot declara (SLOTS do core; hoje false em todos — {% e {# são texto). Só string é ambígua; {{ e }} ocupando a string inteira preserva o tipo de e; texto com ilha rende string; o que não parseia, não rende valor ({{ }}) ou não volta pelo fromJson do @tslite/json fica texto. A string é derivada uma vez: a forma serializada vai para def — a ilha é o nó como está, normalizada pelo @tslite/estree (sem posição e sem o type-node sintático; o Schema de cada anotação fica) —, e as islands saem em memória, com loc no texto do campo — é o que o check tipa com span, sem reparsear;
  • com o profile de quem executa, acusa o que o bind recusaria — pela mesma lista que o compile do @tslite/interpret usa para recusar (compileViolations): nó fora do profile e o que o sanitize barra, com a precedência de lá. Sem profile, isso não é julgado: quem só traduz ou tipa não executa. Uma bancada compila com o DEFAULT_PROFILE, que é o do binder, e a recusa aparece no campo, e não no console;
  • onde cabe expressão é a tabela de slots (FIELDS/SLOTS/TREE, exportada pelo core): props, events[*].params, placement, each, when. Campo fora dela não tem porta.

A marca da ilha (EXPR_MARKER) e o predicado (isExprMarker) são do @tslite/ast, e é de lá que o @loom-forge/check e o eject os importam — a pergunta "isto é uma ilha?" tem uma resposta só, e ela é do dono do marcador.


createTsliteBinder(opts?) — o bind

import { createTsliteBinder } from "@loom-forge/tslite";
const engine = createEngine({
  binder: createTsliteBinder({ globals: { upper } }),
});

Expande a definição contra data: {{expr}} em strings (parser TS-like) e ilhas #expr (serializável). Pipeline 100% TSLite, sem new Function: a fase compile() traduz o texto em ilha #expr; o bind faz fromJson → compile (gateado pelo CapabilityProfile + sanitize) → runner.

Os params de uma intenção não são avaliados no render. Um evento (events.onX), um efeito e cada elo da cadeia then/catch (ADR-018 do root) saem do bind com o params compilado e o escopo do render guardado no binding; quem avalia é o despacho, sobre esse escopo mais a raiz event — o valor emitido pelo widget, o data de um then, o error de um catch. É o intentParams do @loom-forge/core, ligando um nó-fantasma pelo mesmo binder. A compilação endereça cada elo pelo caminho dele (#0.events.onClick.then.params.valido).

interface TsliteBinderOptions {
  globals?: Record<string, unknown>; // helpers no escopo
  profile?: CapabilityProfile; // o que a definição pode programar (default: DEFAULT_PROFILE)
  boundary?: { runtime }; // a metade de runtime de um `createBoundary` do @tslite/validate
  budget?: Budget | false; // custo por avaliação: { steps?, depth?, size? } (default: EXPRESSION_BUDGET)
  onError?(error, source); // falhou ao EXECUTAR (unknown-name, host lançou, orçamento estourado)
  onReject?(error, source); // recusada na COMPILAÇÃO (node-not-allowed, sanitize)
}

Seguro por construção: o compile só vê o escopo passado — globais (fetch/process) não existem. Política = dado: o CapabilityProfile é serializável e viaja com a definição. O default, DEFAULT_PROFILE (PRESETS["pure-fp"]), libera expressão, chamada ao host e lambda — inclusive com corpo em bloco — e fecha mutação, laço, throw e async.

Com lambda, uma expressão consegue não terminar, e por isso o bind vem com orçamento: o EXPRESSION_BUDGET do @tslite/capability (steps 1 milhão · depth 256 · size 10 milhões) entra quando nem a opção nem o profile trazem um. Os números foram medidos aqui (ADR-114) e provados lá: um filter sobre 10 mil linhas custa ~90 mil passos, e uma recursão infinita para em depth. Apertar ou afrouxar é trocar o profile ou o orçamento, não ligar flags:

import { PRESETS } from "@tslite/capability";
createTsliteBinder({ profile: PRESETS.data }); // fecha a lambda: "JSON com lógica"
createTsliteBinder({ budget: { steps: 10_000, depth: 32 } }); // orçamento mais apertado
createTsliteBinder({ budget: false }); // sem contagem, e sem proteção contra recursão sem fim

O compile também roda o sanitize fail-closed por default: a referência à autoridade ambiente (eval, Function, process, globalThis…) é recusada. Nome de chave nenhum é: a Linguagem não tem protótipo, um valor é dado e só tem as chaves que declara, e constructor ou __proto__ são nomes como quaisquer outros — ausentes, valem undefined. Ver docs/ARCHITECTURE.

Diretivas de template (each / when)

Estruturais (mudam a árvore, não só valores) — tornam listas/condicionais declarativas no JSON:

{
  "type": "List",
  "children": [
    // each array → repete o nó por item; o item ganha o nome do `as` (+ `$index`)
    {
      "type": "Row",
      "each": "{{ data.users }}",
      "as": "u",
      "props": { "name": "{{ u.name }}" },
      // when → dropa se falsy (por item dentro do each)
      "when": "{{ u.active }}",
    },
  ],
}
// each objeto re-escopa a subárvore: { "each": "{{ data.user }}", "as": "u", "props": { "n": "{{ u.name }}" } }
  • ids dos repetidos são estáveis: row.${item.id ?? $index}.
  • as é obrigatório — sem ele o item não teria nome, e a compilação acusa missing-binding.
  • as diretivas são consumidas (não aparecem na saída resolvida) e guardadas (mesma Linguagem do bind);
  • o escopo do item estende o do pai — um each aninhado enxerga o item de fora, e a raiz data continua alcançável de qualquer profundidade.

A camada de tipos não está aqui — vive em @loom-forge/check, para o bind de runtime nunca arrastar @tslite/checker/json-schema/infer. Ali é que se prova que um when rende boolean e que {{ item.nomee }} não existe na coleção.


Stdlib

O escopo carrega os 53 operadores do @tslite/std por default (length, map, filter, prop, pick…). Não é conveniência: a Linguagem é FP e não tem protótipo, então xs.length é erro e a forma é length(xs). Sob o profile default a composição point-free funciona inteira — map(prop("nome"), xs), filter(propEq("ativo", true), xs) —, e a lambda também: map((p) => p.nome, xs). A ordem é a do Ramda, com a função primeiro.

O mesmo vocabulário aparece tipado no @loom-forge/check (vanilla, as assinaturas) — é o que faz o checker provar o que o executor vai rodar.

O asserter, quando há um

O check pode tipar {{ parseAs(Address, props.bruto) }} contra um catálogo de tipos nomeados. Tipar é metade; provar é runtime, e runtime é aqui. As duas metades saem de uma declaração — o createBoundary do @tslite/validate monta, do mesmo catálogo, o que o checker precisa e o que o interpret precisa —, e o host entrega a metade certa a cada lado:

import { createBoundary } from "@tslite/validate";
import { vanilla } from "@tslite/std";

const boundary = createBoundary({
  types: { Address },
  reserved: Object.keys(vanilla),
});
createTsliteBinder({ boundary }); //             a metade de runtime: o catálogo e o `parseAs`
buildEnv(def, { boundary }); // no `check`:     a metade de tipos, do mesmo objeto

Não vem embutido: quem não usa asserter não deve pagar o validador, e este package não o importa — a opção recebe só o runtime da fronteira. Ver a fronteira do unknown no @loom-forge/check.