@gwigz/slua

Library

Typed client, runtime events, and target resolution

Everything the CLI does is available as a library, with the same protocol handling, addressing and target precedence.

npm install --save-dev @gwigz/slua-viewer-client

Pushing a script

import { readFile } from "node:fs/promises"
import {
  diagnosticsFrom,
  parseObjectRef,
  resolveItem,
  ViewerClient,
} from "@gwigz/slua-viewer-client"

const client = await ViewerClient.connect({ port: 9020 })

try {
  const target = await resolveItem(client, parseObjectRef("desc:slua:my-project/Main"))

  const result = await client.objectContentSave({
    primId: target.primId,
    itemId: target.itemId,
    content: await readFile("dist/main.slua", "utf8"),
    vm: "luau",
  })

  if (result.compiled === false) {
    console.error(diagnosticsFrom(result, "luau"))
  }
} finally {
  // An open socket keeps the process alive, so this happens either way.
  client.close()
}

The viewer drives the handshake, so this is a bidirectional peer rather than a plain client: it answers session.handshake (including the local-file auth challenge), session.ping, and any editor.* commands you register through the commands option.

Methods

Each method is named after the RPC it sends:

AreaMethods
ObjectsobjectList, objectRequest, objectUnpublish, objectModify
ContentobjectContentGet, objectContentSave
ItemsobjectItemCreate, objectItemDelete, objectItemModify
ScriptssetScriptRunning, resetScript, scriptList, scriptSubscribe
LanguagesyntaxId, syntax, syntaxCache, syntaxGet
CommandsexecuteCommand, listCommands

objectContentSave allows the viewer's full 60s upload budget, other calls use the connection's timeout.

Runtime events

Runtime output is only forwarded for published objects, so publish before you listen:

import { ensurePublished, ViewerClient } from "@gwigz/slua-viewer-client"

const client = await ViewerClient.connect()

await ensurePublished(client, { kind: "id", value: "4f2b0c1e-0000-0000-0000-000000000000" })

client.on("runtime.error", (event) => console.error(event.objectName, event.message))

on returns an unsubscribe function, and runtime.debug carries the script's non-error output. A viewer advertising unifiedDiagnostics fills in error, line and an item reference naming the script. Older ones leave error empty and line 0, and send the text as a separate runtime.debug message. line is 0 on any viewer when the error names no line.

ensurePublished and resolveItem take options for the awkward parts:

OptionEffect
waitMsWait this long for the viewer to publish a match rather than failing
onWaitCalled once when a wait begins, so a CLI can say what it is waiting for
timeoutMsHow long an object.request has to be honoured

resolveItem also re-reads the listing a few times before giving up, since the viewer's published inventory goes briefly stale right after a save.

Target resolution

Target resolution is exported too:

import { loadConfig, readHeaderTagsFor, resolveTarget } from "@gwigz/slua-viewer-client"

const config = await loadConfig()

const target = resolveTarget({
  name: "main",
  config: config?.targets.main,
  header: await readHeaderTagsFor("dist/main.slua"),
  configRoot: config?.root,
})

readHeaderTagsFor follows the source map beside a built file back to the source that produced it, so header tags work on bundled output. loadSourceMapFor and SourceMap.mapRow are exported for mapping Lua rows yourself.

Errors

Failures throw typed errors rather than plain strings:

ErrorMeaning
ViewerUnavailableErrorNothing listening, or the port is wrong
HandshakeErrorConnected, but the viewer refused the session
ConnectionClosedErrorThe viewer closed the connection mid-request
RpcTimeoutErrorNo response inside the timeout
RpcErrorThe viewer returned a JSON-RPC error, see code

Naming

The viewer's protocol is snake_case, and everything this package exposes is camelCase, converted once at the JSON-RPC boundary. So object_id on the wire is objectId in your code and in --json output. Only field-shaped keys convert. Keys carrying data, an item named Main or an object named Door Control, pass through untouched.

On this page