@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
Maintainers
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-serviceOs @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çãoO 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 slotsO 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.
