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

@inhuman.tools/arrival-serializer

v0.9.9

Published

S-expression serializer for Arrival - converts JavaScript objects to Scheme/Lisp representations

Readme

@inhuman.tools/arrival-serializer

Standalone JavaScript → s-expression serializer for LLM-facing text. It is not a lossless round-trip.

Serialization is two stages: toSExpr builds an s-expression (a plain JS array IR you can spread one representation inside another), and formatSExpr pretty-prints it. Custom types control their own form through Symbol.toSExpr / Symbol.SExpr, which come from the @here.build/arrival-env runtime dependency.

Installation

pnpm add @inhuman.tools/arrival-serializer

@here.build/arrival-env is a runtime dependency (installed with this package). Import it wherever you implement Symbol.toSExpr / Symbol.SExpr.

Quick Start

import { toSExpr, formatSExpr, toSExprString } from "@inhuman.tools/arrival-serializer";

toSExprString(42); // 42
toSExprString("hello"); // hello
toSExprString("with space"); // "with space"
toSExprString([1, 2, 3]); // [1 2 3]
toSExprString({ x: 10, y: 20 }); // {:x 10 :y 20}

Quoting is lexical: strings with spaces or special characters get quotes; identifier-like tokens stay bare. Quoting is not a computed-vs-literal signal.

import "@here.build/arrival-env"; // Symbol.toSExpr / Symbol.SExpr
import { toSExprString } from "@inhuman.tools/arrival-serializer";

class Point {
  constructor(
    public x: number,
    public y: number,
  ) {}

  [Symbol.SExpr]() {
    return "Point";
  } // optional head-tag override
  [Symbol.toSExpr](ctx) {
    return [ctx.keyword("x"), this.x, ctx.keyword("y"), this.y];
  }
}

toSExprString(new Point(10, 20)); // (Point :x 10 :y 20)

Custom Serialization

Implement Symbol.toSExpr(ctx) returning an array of parts. The serializer prepends a head tag and recurses into every part, applying Symbol.toSExpr again where present and otherwise following standard rules. You never wrap properties — a plain object among the parts auto-converts to {:key value …}.

The head tag resolves through this fallback chain (see the dispatch site in serializer.ts):

this[Symbol.SExpr]?.() ?? this.displayName ?? this.constructor.displayName ?? this.name ?? this.constructor.name

The ctx object provides the leaf constructors:

  • ctx.symbol(value) — unquoted symbol
  • ctx.keyword(value) — :value
  • ctx.quote(value) — 'value
  • ctx.string(value) — "value"
  • ctx.expr(head, ...args) — (head arg1 arg2)

Optimizing Representations

Baseline output is already at least as compact as JSON, so optimize only the hottest, most data-heavy entities. Each technique shrinks a representation while adding meaning; applied systematically at data-heavy locations they yield materially fewer tokens than minified JSON and stay more readable than formatted JSON.

  • Type + name + id — (User 'abc123 "John Doe"): the head names the entity type, a quoted symbol marks the identifier, a string carries the display name.
  • Flags as keywords — emit a bare :free-tier when a boolean's presence alone is meaningful, and omit absent fields entirely (the same technique carries clear tags like :div / :section on render-tree nodes).
  • Named collections — (members (User …) (User …)) for an unordered collection field, rather than a positional list.
  • Views — represent a referenced entity by a compact projection ((User 'abc123 "John Doe")) instead of inlining it in full. Doubles as cyclic-reference defense; a view may also project a larger structure when detail is the point (e.g. a component render tree).

Why named heads and quoted identifiers read well: a named head gives a semantic anchor the way a variable name does over a raw address. High-entropy strings like UUIDs are naturally read as pointers, and the 'abc123 quote signals "reference, not data" — the same cognitive pattern as pointers in code, which models trained on code recognize immediately.

Whether these forms also improve AI consumption (not just density) is unvalidated — a domain worth researching.

API

  • toSExpr(obj): SExpr — value → s-expression IR.
  • formatSExpr(sexpr, indent?): string — IR → pretty-printed string.
  • toSExprString(obj, indent?): string — the two combined.
  • toSExprStringWithElisions(obj, opts?): { text, elisions, reduced } — same walk as toSExprString, plus middle-elision records and reduced (true iff this render dropped content: tail-truncation, middle-elision, string cap, or hard-cut). Caps requested but everything fit ⇒ reduced: false.
  • serializeWithExtras(value, opts?): { core, extras, overflow } — same walk, extracting binary leaves (Blobs) into extras and leaving #attachment tags in core.
  • sexpr(tag, ...args): SExprDefinition — tagged s-expression.

Type Mappings

| JavaScript | S-Expression | | ----------- | ------------------------------------------------------------------------------ | | null | nil | | undefined | undefined | | Numbers | Numbers (with BigInt support) | | Strings | Unquoted when identifier-like; "quoted" when they contain spaces or specials | | Booleans | #t / #f | | Arrays | [...] | | Objects | {:key value ...} | | Symbols | :keyword | | Map | (map :key value ...) | | Set | (set ...) | | Date | ISO string |

Scheme Integration

Built-in support for @inhuman.tools/arrival's Scheme runtime types (duck-typed by constructor name, exposed from @inhuman.tools/arrival/reflect-internals):

  • AExact (exact integers / rationals) → Numbers or num/denom
  • AInexact (floats / complex) → Numbers or real+imagi
  • ASymbol → Symbols/keywords
  • AString → single-quoted or template strings
  • ACharacter → character literals (#\char)
  • APair / ANil → lists
  • Values → multiple return values
  • EOF, Macro, Syntax, InputPort/OutputPort → reader-friendly placeholders

Related

  • @here.build/arrival-env — Symbol.toSExpr / Symbol.SExpr protocol (runtime dependency of this package)
  • @inhuman.tools/arrival — the Scheme interpreter, exposing runtime types (e.g. via /reflect-internals)

License

MIT.