@nx-lang/language-protocol

中文文档
0.10.0发布于 2026/11/02MITTypeScriptES Module零依赖构建来源已验证

Transport-independent request and answer shapes for NX language queries, and the NxLanguageService interface every client implements.

原始使用文档
文档目录12 节
  1. The document-set model
  2. Positions are UTF-16 code units
  3. Queries
  4. hover
  5. completions
  6. diagnostics
  7. documentSymbols
  8. sourceTree (unstable)
  9. sourceEdit (unstable)
  10. Reserved names
  11. The service interface
  12. Errors

@nx-lang/language-protocol

The contract between an editor and the NX language service: the shape of a language query and its answer, the position encoding both sides use, and the NxLanguageService interface every client implementation satisfies. Types, a few constants and guards, and the error classes; no runtime dependency.

Implementations of the interface live elsewhere:

  • @nx-lang/language-client — an HTTP implementation for browsers, talking to
  • @nx-lang/language-http — a mountable Node handler that answers queries through @nx-lang/sdk-node.

An editor integration such as @nx-lang/monaco depends on this package alone, so an HTTP-backed, worker-backed, or in-process service is interchangeable.

The document-set model

Every query carries the complete set of documents it is to be answered from, plus the URI of the document the query is about:

interface LanguageQuery {
  documents: { uri: string; source: string; version?: number; identity?: string }[];
  uri: string; // one of documents[].uri
}

The service keeps nothing between queries. Two identical queries produce identical answers whatever was asked in between, which is what lets several server instances answer without session affinity. A single-document host sends its one document; a host editing one file of a set that import each other sends the whole set, with the edited document carrying the editor's unsaved text, and completions include what the edited document imports from its siblings.

version is whatever the editor counts; it is echoed back in the answer so a client can discard an answer that arrives after a newer version was sent. identity is the workspace identity NX resolves imports by (tenant/form.nx); it is derived from the URI when omitted.

Positions are UTF-16 code units

A TextPosition is a zero-based line and a zero-based character counted in UTF-16 code units from the line start, the way String.prototype.length counts and the way Monaco, CodeMirror and the Language Server Protocol default count. An emoji earlier on the line is two units. A range carries its start and end positions and, alongside them, startByte and endByte offsets into the document's UTF-8 text, which is what the language service measured.

// let s = "😀" + name
//                ^ name starts at character 15 (the emoji is 2 units) and byte 18 (it is 4 bytes)
{ start: { line: 0, character: 15 }, end: { line: 0, character: 19 }, startByte: 18, endByte: 22 }

Queries

Five queries are defined; each has a request and an answer type that round-trip through JSON. The examples below are compiled in test/examples.test.ts, so they cannot drift from the types.

hover

const request: HoverRequest = {
  documents,
  uri: "nx://tenant/form.nx",
  position: { line: 1, character: 3 },
};

const answer: Hover | null = {
  uri: "nx://tenant/form.nx",
  identity: "tenant/form.nx",
  version: 7,
  range: { start: { line: 1, character: 1 }, end: { line: 1, character: 7 }, startByte: 34, endByte: 40 },
  contents: "```nx\n<Button label:string />\n```",
};

contents is markdown with NX fragments in ```nx fences. When the service has nothing to say the answer is null, not an error.

completions

const request: CompletionsRequest = {
  documents,
  uri: "nx://tenant/form.nx",
  position: { line: 1, character: 23 },
};

const answer: CompletionList = {
  uri: "nx://tenant/form.nx",
  identity: "tenant/form.nx",
  version: 7,
  items: [{ label: "label", kind: "Property", detail: "label:string", documentation: "The caption." }],
};

kind is one of Keyword, Type, Declaration, Component, Property, Member. documentation is the /// documentation of what the item names, as markdown, and is absent when it has none.

diagnostics

const request: DiagnosticsRequest = { documents, uri: "nx://tenant/form.nx" };

const answer: DiagnosticReport = {
  documents: [
    { uri: "nx://tenant/ui.nx", identity: "tenant/ui.nx", version: 3, diagnostics: [] },
    {
      uri: "nx://tenant/form.nx",
      identity: "tenant/form.nx",
      version: 7,
      diagnostics: [
        {
          range: { start: { line: 1, character: 8 }, end: { line: 1, character: 13 }, startByte: 41, endByte: 46 },
          severity: "Error",
          code: "type-mismatch",
          message: "Expected string, found int",
          related: [],
        },
      ],
    },
  ],
  workspace: [],
};

The report covers every document in the set, one entry each, so a multi-document host can set markers on all of them from one answer. workspace holds diagnostics that belong to no document, such as an import of a module that is in neither the set nor a library. severity is one of Error, Warning, Info, Hint.

documentSymbols

const request: DocumentSymbolsRequest = { documents, uri: "nx://tenant/ui.nx" };

const answer: DocumentSymbol[] = [
  {
    name: "Button",
    kind: "Component",
    range: { start: { line: 0, character: 0 }, end: { line: 0, character: 47 }, startByte: 0, endByte: 47 },
    selectionRange: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 }, startByte: 12, endByte: 18 },
  },
];

kind is one of Function, Value, TypeAlias, Record, Action, Union, Component, Element.

sourceTree (unstable)

Unstable: the answer's shape may change in any release until a later one commits to it. It is published for the NX viewer and a structural change view to build on.

const request: SourceTreeRequest = { documents, uri: "nx://tenant/ui.nx" };

const answer: SourceTree = {
  uri: "nx://tenant/ui.nx",
  identity: "tenant/ui.nx",
  version: 3,
  nodes: [
    { role: "declaration", range: range(0, 47), key: "Button", name: "Button", declaration: 0, flags: ["export"] },
    { role: "parameter", range: range(19, 31), parent: 0, key: "Button.label", name: "label" },
    { role: "typeReference", range: range(25, 31), parent: 1, key: "Button.label.type", value: "string" },
    { role: "element", range: range(37, 47), parent: 0, key: "Button.body", name: "button", type: "button" },
  ],
  declarations: [
    { module: "tenant/ui.nx", name: "Button", kind: "function", range: range(0, 47), properties: [{ name: "label", type: "string" }] },
  ],
};

(range(start, end) stands for the EditorRange of those bytes on line 0.)

Every piece of the document is a node, comments and regions that did not parse included, and every token belongs to exactly one node: the smallest whose range holds it. Nodes are listed in order of start offset, each parent before its children, with the index of the parent. A node has a role (element, attribute, literal, case, operator, condition, comment, unparsed and the rest of SourceRole), and carries what it says as written in name, value and textType, its type spelled in NX in type when the type checker gave it one, and in declaration the index of the declaration it refers to. A node's type and declaration are what hover reports inside it.

key is the node's path within the document, such as roleQuestion.value.choices[2].label: the top-level declaration's name, then the attributes, members, arms and slots on the way, and positions for items. An edit to another declaration or another attribute does not change it, so two versions of a document can be compared by key. A key is an identifier to compare for equality, not a path to split: an arm is named by its patterns or test as written (is Mode.dark, when x > 1) and an import by its path, and those may hold dots and spaces. Read the tree's structure from parent.

declarations lists once each declaration a node refers to, from this document, another document of the set or a library: its module identity, name and kind, its range when this document declares it, its doc comment, its properties in declaration order with inherited ones first (each with its type, its default as written, and whether it is optional, content or inherited), its bases, a component's state, a union's cases, an alias's type, and a value's type and the range of its value. Optional properties of a node or a declaration are absent rather than null when they do not apply.

A request with visualizers: {} (or visualizers: { modules: [...] }, naming further visualizer modules the host supplies) also resolves visualizer declarations: each declaration that has one carries a visualizer, with its title, id and icon, optional preview: { renderer: string }, its properties in the order to show them, and, for each property or alias, a format and an editor. editor holds the editor hints that fit the type: kind (field, dropdown or segmented), min, max, step, multiline, placeholder and choices. Each choice's value is typed as a sourceEdit setValue value, so a client passes it on as it is to write the choice:

const sizeProperty: SourcePropertyVisualizer = {
  name: "size",
  editor: {
    max: 96,
    choices: [
      { value: { kind: "number", text: "12" }, label: "sm" },
      { value: { kind: "number", text: "16" }, label: "base" },
    ],
  },
};

sourceEdit (unstable)

Unstable: the request's and the answer's shapes may change in any release until a later one commits to them. It is published for the NX viewer's Visual editing to build on.

const request: SourceEditRequest = {
  documents,
  uri: "nx://tenant/ui.nx",
  key: "Button.label.type",
  edit: { kind: "setValue", value: { kind: "string", value: "Save" } },
};

const answer: SourceEditAnswer = { edits: [{ range: range(25, 31), newText: '"Save"' }] };
const refused: SourceEditAnswer = { edits: [], refused: "notEditable" };

The request names a node of the document's source tree by its key and one change: setValue with a typed value (string, number as typed, boolean, or case by name) for a literal or a bare union case that is a whole value, setText with one line of text for a run of text content, remove for an attribute or for an element that is an item of a list, addProperty with a property's name and a typed value for an element, which writes the attribute after the element's last one, in its layout, or move with a position to for an element that is an item of a list. An item of a list is an element in a braced sequence or in an element's content, not in an if branch or a for body, in a list that holds no text; to is the position it takes once moved, counted as the list's keys count its items. In a list written an item to a line, an item moves or goes with its whole lines, the comments directly above it, and the blank lines that separate it from the next item (or the one before, for the last); in a list on one line, with the space beside it. Other layouts are refused notEditable, and a to outside the list invalidValue. The answer's edits make the change: every range is in the document as sent and no two overlap, so a host applies them together; the service writes the spelling (quotes, escapes, kept braces, an attribute's whole line). A change it cannot make answers no edits and refused: unknownNode, notEditable or invalidValue. A change that would leave the text as it is answers no edits and no refusal.

Reserved names

definition, references, rename, signatureHelp, inlayHints and semanticTokens are reserved. An implementation that receives one answers with the unsupported-query error rather than a transport failure or a malformed answer, so a client can tell "not yet" from "broken". isLanguageQueryName and isReservedLanguageQueryName classify a name.

The service interface

interface NxLanguageService {
  hover(request: HoverRequest, signal?: AbortSignal): Promise<Hover | null>;
  completions(request: CompletionsRequest, signal?: AbortSignal): Promise<CompletionList>;
  diagnostics(request: DiagnosticsRequest, signal?: AbortSignal): Promise<DiagnosticReport>;
  documentSymbols(request: DocumentSymbolsRequest, signal?: AbortSignal): Promise<DocumentSymbol[]>;
  sourceTree(request: SourceTreeRequest, signal?: AbortSignal): Promise<SourceTree>; // unstable
  sourceEdit(request: SourceEditRequest, signal?: AbortSignal): Promise<SourceEditAnswer>; // unstable
}

Cancelling signal before the answer arrives rejects the promise with an abort error, which isAbortError recognizes, and the promise never resolves afterwards. An editor integration is tested against an in-process fake of this interface and behaves the same with the HTTP client for identical answers.

Errors

ClassWhen
UnsupportedQueryErrorThe service recognized the query name but does not support it. Carries query.
LanguageServiceHttpErrorAn HTTP implementation received a non-success status. Carries status, url, a bodyExcerpt, and the body's code when it was an error body.
LanguageServiceTimeoutErrorThe request did not answer within the client's timeout; the request was aborted. Carries timeoutMs.

An HTTP implementation answers a failed query with the JSON body

{ error: { code: LanguageServiceErrorCode; message: string; query?: string } }

where code is one of unsupported-query, unknown-query, method-not-allowed, invalid-request, payload-too-large, internal-error. isLanguageServiceErrorBody recognizes the shape.

Source-tree callers can set visualizers: { includeUnreferencedDeclarations: true } to include loaded visualizer targets and inherited presentation metadata even when the entry source does not reference them directly. The default keeps the declaration table bounded to the source tree. Unknown preview renderer IDs remain metadata; hosts activate only their supported implementations.

版本与发布记录

@nx-lang/language-protocol 共发布 10 个版本,首次发布于 2026/09/15;过去 12 个月发布了 10 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.10.0latest2026/11/021 天前
0.9.02026/10/268 天前
0.8.02026/10/1816 天前
0.7.02026/10/1024 天前
0.6.02026/10/0628 天前
0.5.02026/09/291 个月前

关于 @nx-lang/language-protocol 的常见问题

@nx-lang/language-protocol 是什么?

NX语言查询的独立于传输的请求和回答形状,以及每个客户端实现的NxLanguageService接口。

如何安装 @nx-lang/language-protocol?

在项目目录运行 npm install @nx-lang/language-protocol,或使用 pnpm add / yarn add / bun add。

@nx-lang/language-protocol 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@nx-lang/language-protocol 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@nx-lang/language-protocol 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@nx-lang/language-protocol 可以商用吗?

@nx-lang/language-protocol 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@nx-lang/language-protocol 还在维护吗?

最新版本 0.10.0 发布于 2026/11/02(1 天前);至今共发布 10 个版本,其中过去 12 个月发布了 10 个;首次发布于 2026/09/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。