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/language-service

v0.4.0

Published

forge · o serviço de linguagem (dev/CI): os diagnósticos da compilação e dos tipos na coordenada do documento (DocumentPath), e o ambiente de tipos de um campo — a projeção da análise que um inspetor ou um canvas consome.

Downloads

369

Readme

@loom-forge/language-service

O serviço de linguagem do construtor: o que um inspetor, um canvas ou um editor de arquivo perguntam sobre um documento de componentes — e a resposta na coordenada que o autor enxerga. Dev/CI, nunca no hot path de render: ele arrasta o @loom-forge/check, e o check arrasta o @tslite/checker.

pnpm add -D @loom-forge/language-service

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.

A tese

Um serviço de linguagem não é dono de nenhuma regra da linguagem. Ele é dono de saber a quem perguntar:

COMPILAÇÃO   @loom-forge/tslite, via o forge     sintaxe, ilha vazia, o que o deploy recusa
TIPOS        @loom-forge/check                    tipo, escopo, call-site, declaração

O que ele acrescenta é o que nenhum dos dois responde: a resposta na coordenada do documento — o DocumentPath do @tslite/language-service, o caminho real dentro do ComponentDef —, e o ambiente de um campo: que nomes o campo enxerga de onde está, com que tipo, projetado da mesma análise que o tipa.

É o mesmo desenho do @node-actions/language-service, sobre outro documento. O que é comum às duas linguagens já vive no contrato do TSLite (DocumentPath, FieldDiagnostic, byField, withFieldDiagnostics); o que é domínio — um each sem as, o slot de um componente, o placement que o layout do pai publica — continua em dois, e é por isso que este package existe separado.

Uso

import { createComponentService } from "@loom-forge/language-service";

const service = createComponentService({
  profile, //   o CapabilityProfile do deploy — default `DEFAULT_PROFILE` do bind
  types, //     o catálogo de tipos nomeados
  contexts, //  o catálogo de contextos (`nome → TypeRef`)
  emitsOf, //   o que cada peça do kit emite num evento (`descriptor.meta.emits`)
  actions, //   a porta de ação do host — a assinatura do que não é update local
  commands, //  o vocabulário de comando do host
});

service.diagnostics(defs); //           FieldDiagnostic[] — compilação + tipos, por campo
service.diagnosticsOf(defs, "Card"); // idem, de um componente
service.envAt(defs, path); //           o Env de um campo — o que o campo de expressão liga
service.compiled(defs, "Card"); //      o artefato que o palco executa, com o que a compilação disse
service.system(defs); //                o SystemIR — grafo, fatos, para quem julga regra
service.mounting(defs); //              o que cada componente instala nos próprios slots

O mundo entra na construção; o documento é argumento. As opções são o que existe fora do documento — o deploy, os catálogos, o kit, o host — e mudam quando alguém publica algo. O documento (readonly ComponentDef[], o sistema inteiro) muda a cada edição, e por isso todo método o recebe: a análise é memoizada pela identidade dele, e não há serviço "uma versão atrás" para sincronizar.

Os diagnósticos — na coordenada do documento

interface FieldDiagnostic {
  path: DocumentPath; // { root: "Card", segments: ["body", "children", 1, "props", "title"] }
  span?: Span; //        relativo ao TEXTO DO CAMPO; ausente = o campo inteiro
  code: string; //       o contrato — `parse-error`, `no-such-member`, `not-assignable`…
  message: string;
  severity: "error" | "warning";
  source: "compile" | "check"; // quem falou (`DIAGNOSTIC_SOURCE`)
}

O caminho é a lista, e é o caminho REAL. 2 é índice, "2" é chave — a distinção fica no tipo, e nenhuma string a preserva. Os segmentos andam o ComponentDef sem tradução: valueAt(def, segments) é o valor do campo, fieldText(def, segments) é o texto dele, e source.slice(span) sobre esse texto é o trecho aceso. formatPath(path) é a exibição (Card.body.children[1].props.title); pathKey(path) é a chave de mapa. Nada é lido de volta.

| onde o campo mora | segments | | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | um slot de um nó | ["body", "children", 1, "props", "title"] | | o fallback de um nó | ["body", "children", 1, "fallback", "when"] | | o set de um update | ["updates", "somar", "set", "n"] | | os params de uma intenção, e da cadeia dela | ["body", "children", 1, "events", "onClick", "params", "email"] · […, "onClick", "then", "params", "valido"] · […, "catch", "params", "motivo"] | | o init de um estado | ["state", "aberto", "init"] | | uma declaração de tipo | ["props", "n"] |

A compilação fala uma vez. O serviço compila cada definição com o profile do deploy e entrega o artefato ao check; o que a compilação recusou (sintaxe, ilha vazia, um nó fora do profile) sai com source: "compile", e o check não o repete. Um campo que não compilou é tipado como ausência de valor — não como o texto que ficou nele.

Para um painel com um campo por slot, byField reparte tudo uma vez por análise, e withFieldDiagnostics entrega ao campo os dele, presos ao texto commitado:

import {
  byField,
  pathKey,
  withFieldDiagnostics,
} from "@tslite/language-service";

const perField = byField(service.diagnostics(defs));
const mine = perField.get(pathKey(path)) ?? [];

O ambiente de um campo — envAt

const env = service.envAt(defs, {
  root: "Lista",
  segments: ["body", "children", 0, "children", 1, "props", "title"],
});
// dentro de um `each … as item`: `item` existe, com o tipo INFERIDO da coleção

É a porta que a receita "o campo de um inspetor" do @tslite/react deixa do lado do domínio. Ela responde pelo que o campo enxerga de onde está:

| o campo | o que enxerga | | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | um slot na raiz do corpo | props, state, ctx (o que o componente usa) | | um slot dentro de um each | o item, inferido da coleção — e os de fora | | o próprio each | o de fora: a coleção é avaliada antes do laço | | o set de um update | params, tipado com o que o update declara | | o init de um estado | só props e ctx — é o que semeia o estado | | os params de um efeito | o escopo do componente | | os params de uma intenção (events.onX, then, catch, e os de um efeito e do cleanup dele) | o escopo do nó (o raiz, num efeito), mais a raiz event — o que a intenção RECEBEU, com o tipo que quem o produziu declarou: o emits do widget, o resultOf da ação anterior num then, o erro do contrato num catch, undefined depois de um update local e num efeito (ADR-018 do root). É o frame do ELO nos frames do check; um campo da intenção que não é params (action, resolve) vê o escopo do nó, sem event | | qualquer campo de um componente VIVO | pending[ação]true do despacho ao resultado de uma ação do host; Record<string, boolean>. Num componente puro não existe |

Ela responde por um campo vazio e por um que não compilou. O ambiente de um campo é função do que vem antes dele no documento, não do texto dele — quebrar o texto do campo não muda quem o campo enxerga. Um campo novo, ainda sem expressão, já completa item. como o vizinho.

E é uma projeção, não uma segunda análise. O check baixa o componente num corpo TSL e o @tslite/checker o tipa; cada nó e cada update têm o bloco em que os slots dele foram emitidos (Lowered.frames), e o scopeAt do checker sobre esse bloco é o escopo do campo. Um serviço que derivasse escopo por conta própria seria a segunda verdade que o ADR-208 do check existe para impedir.

Com o env na mão, o campo é o do TSLite:

import { createHostService } from "@tslite/editor";
import { mustache, withFieldDiagnostics } from "@tslite/language-service";

const base = createHostService({ env, segmenter: mustache, mode: "expression", profile });
const field = withFieldDiagnostics(base, { text: committed, diagnostics: mine });
<ExpressionInput {...expressionFieldProps({ layout: "grow" })} service={field} value={committed} onCommit={…} />;

Este package não importa o @tslite/editor: ele responde em domínio (Env, diagnóstico, caminho), e quem monta a superfície — CodeMirror, Monaco, canvas — compõe.

O que ele recusa na montagem

Um catálogo de contextos que não resolve (contexts: { "@app/x": "numero" }) é erro de quem monta o sistema, não diagnóstico do programa: createComponentService lança LanguageServiceError (code: "invalid-context-catalog", com os issues) — a mesma régua do createBoundary do @tslite/validate para a colisão de nome.

Exports

createComponentService · DIAGNOSTIC_SOURCE · LanguageServiceError · documentPathOf · segmentsOf · pathOf · nodePrefixOf · intentPrefixOf · os tipos ComponentService, ComponentServiceOptions, Document.