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

@axis-dsl/viewer

v2.5.0

Published

Every Axis React component: the results panel and the Desmos graph and JSON inspector it is built from, shared by the web playground and the VSCode preview, and the protocol that drives it

Readme

@axis-dsl/viewer

The Axis results panel: a live Desmos graph, and — in debug — the JSON behind it beside it. Also the protocol every host drives it with, and the transports that carry that protocol, at @axis-dsl/viewer/protocol.

npm install @axis-dsl/viewer react react-dom

react is a peer dependency. Desmos itself is loaded at runtime from desmos.com using the API key you supply — nothing to install.

Usage

import { useMemo } from 'react';
import { compileAxis } from '@axis-dsl/compiler';
import { AxisViewer, useLocalViewerHost } from '@axis-dsl/viewer';

function Preview({ source }: { source: string }) {
    const { state, options } = useMemo(() => compileAxis(source), [source]);
    const transport = useLocalViewerHost({
        apiKey: MY_DESMOS_KEY,
        state,
        options,
        status: `${state.expressions?.list?.length ?? 0} expressions`,
    });

    return <AxisViewer transport={transport} />;
}

A graph is two things, state and options, because Desmos has two ways in: the viewer applies them as calculator.setState(state) and then calculator.updateSettings(options), and does nothing else to them. Everything the graph is made of — the expression list, the ticker, the viewport, the top-level flags — is in state, so a graph looks the same in every host that shows it.

Debug mode

The viewer is the graph and nothing else by default: no tabs, no status line, the calculator filling whatever it is given. debug adds the Graph/JSON tabs and the status the transport reports, for a host that is a workbench rather than a preview.

<AxisViewer transport={transport} debug />

The graph is the same either way — the JSON pane is what debug adds, so the status a host reports has somewhere to appear only when it is on.

One way in

AxisViewer has no props for the graph, the API key or the status. Everything it displays arrives as a ViewerMessage over a ViewerTransport, and everything it asks for goes back as a HostMessage. One path in means a feature is built once and every host gets it — the VSCode preview over an HTTP event stream, a web playground over an in-memory channel. Same viewer either way.

type ViewerMessage =
    | { command: 'init'; data: { desmosApiKey: string; canSetApiKey?: boolean } }
    | { command: 'setGraph'; data: { state: GraphState; options: CalculatorOptions } }
    | { command: 'setStatus'; data: { status: string | null } }
    | { command: 'setSync'; data: { enabled: boolean } };

type HostMessage =
    | { command: 'ready' }
    | { command: 'requestApiKey' }
    | { command: 'graphChanged'; data: { before: GraphReading; after: GraphReading } };

The viewer sends ready on mount; the host answers with init and the current graph. setGraph replaces the graph whole, and one that is the same as the last is not applied again, so re-sending an unchanged graph leaves the viewport wherever the user panned it. requestApiKey is only ever sent to a host that set canSetApiKey, because only a host with somewhere to put a key can act on it — the extension opens VSCode settings, a host with one baked in has nowhere. Pass onRequestApiKey to useLocalViewerHost and the viewer offers a "Set an API key" button that calls it; leave it out and the button is not rendered at all, rather than leading nowhere.

The messages and transports live at @axis-dsl/viewer/protocol, a subpath that imports neither React nor the DOM when loaded, so the other end of the wire can be a Node process: the extension's preview server imports it in the VSCode extension host. The package root re-exports the protocol's types for a React host that wants to name them.

In-process

useLocalViewerHost is the transport for a host that renders the viewer itself, and is what a React host should reach for first. Underneath it is createLocalChannel, two ends of a synchronous channel with no wire between them:

import { createLocalChannel } from '@axis-dsl/viewer/protocol';

const { host, viewer } = createLocalChannel();

host.onMessage(message => {
    if (message.command === 'ready') {
        host.send({ command: 'init', data: { desmosApiKey: key } });
        host.send({ command: 'setGraph', data: { state, options } });
    }
});

<AxisViewer transport={viewer} />;

Across a wire

createHttpTransport is the viewer's end of a connection to the extension's preview server: Server-Sent Events downstream, a POST per message upstream. It defaults every option out of the page's own URL, so the page that loads the viewer usually needs no arguments:

import { createHttpTransport } from '@axis-dsl/viewer/protocol';

<AxisViewer transport={createHttpTransport()} />;

SSE rather than a WebSocket because the traffic is almost entirely one-way — the viewer sends two messages in its life — and it needs no dependency on either end. It also reconnects on its own.

A transport with a wire can report whether it still has one, through the optional onConnectionChange. A momentary drop reads as connecting and recovers silently; only a stream that spends reconnectGraceMs (3s by default) failing to come back is called disconnected, at which point the viewer says so above the graph rather than leaving a stale one looking current. A first connection is silent. A transport that omits onConnectionChange, as the in-process channel does, is taken to be connected for as long as it exists.

PREVIEW_PATHS and PREVIEW_QUERY are the preview server's HTTP surface — the routes and the query keys — kept here because both ends depend on this package and neither can see the other at runtime. PREVIEW_QUERY.token is a per-session secret every route requires: any process on the machine can reach a loopback port.

Editing from the graph

Pass onGraphChanged to useLocalViewerHost and the viewer watches the calculator for what the user does to the graph directly — dragging a point, moving a slider, recolouring something, panning — and hands back two readings: the graph as it was when your graph was applied, and the graph now. Over the protocol this is setSync turning on graphChanged.

const transport = useLocalViewerHost({
    apiKey,
    state,
    options,
    onGraphChanged: (before, after) => {
        const { edits } = writeBackGraph(source, { before, after }, compiled);
        setSource(applySourceEdits(source, edits));
    },
});

Leave it out and the calculator is not watched at all — the viewer only starts looking when a host says it has somewhere to put the answer.

The difference between the two readings is the user's doing and nothing else. The baseline is read back off the calculator rather than taken from the state that produced it, because Desmos normalises what it is given: a state compared against what was sent would report a change on every expression the moment the graph loaded. Reports are debounced, since a drag is hundreds of change events and one edit.

A reading is a GraphReading — { state, options }, calculator.getState() and a copy of calculator.settings, the same two halves a graph is applied as. That is what writeBackGraph in @axis-dsl/compiler compares, and it is what turns the pair into edits to the statements that produced them.

Capturing an image

A ref on AxisViewer hands back the graph it owns, whose capture() renders the graphpaper to a data URI — the expression list is never in it. It resolves null until Desmos has loaded and the calculator exists, so the affordance can be offered before knowing whether it has.

const viewer = useRef<AxisViewerHandle>(null);

async function download() {
    const png = await viewer.current?.capture({ width: 1200, height: 800, targetPixelRatio: 2 });
    if (png) {
        // …it's yours: save it, put it on the clipboard, upload it as a thumbnail
    }
}

return <AxisViewer ref={viewer} transport={transport} />;

The options are Desmos' own asyncScreenshot options (width, height, targetPixelRatio, format, mode, mathBounds, showLabels, preserveAxisNumbers), typed as AsyncScreenshotOptions in @axis-dsl/desmos. Passing none captures the graph as it is on screen. getGraph() on the same handle reaches the rest of DesmosGraphHandle — getCalculator(), getExpressions(), getState().

Theming

The viewer brings its own palette and looks the same in every host — nothing needs configuring, and it follows the OS light/dark setting on its own. The palette is published as AXIS_THEME, a set of --axis-* custom properties applied to the panel's root element:

--axis-fg  --axis-fg-muted  --axis-surface  --axis-surface-raised
--axis-border  --axis-accent  --axis-accent-fg  --axis-danger
--axis-font  --axis-font-size  --axis-mono  --axis-mono-size

A host that needs different colours passes any of them in style, which is applied after the theme and therefore wins:

<AxisViewer transport={transport} style={{ '--axis-accent': '#c2410c' }} />

API

| Export | | | --------------------------- | ----------------------------------------------------------------------------------- | | AxisViewer | The panel. Props: transport, debug?, ref?, className?, style? | | useLocalViewerHost(host) | Turns { apiKey, state, options, … } into protocol messages; returns the transport | | AxisViewerHandle | What its ref exposes: capture(options?), getGraph() | | DesmosGraph | Just the graph, if you want to arrange things yourself | | JsonInspector | Just the JSON pane | | useDesmos(apiKey) | Loads the Desmos script once per page | | useViewerState(transport) | The state AxisViewer builds from the messages | | AXIS_THEME | The palette, as --axis-* custom properties | | AXIS_COLOR_SCHEME | The color-scheme those properties need to resolve |

From @axis-dsl/viewer/protocol:

| Export | | | ----------------------------------------------- | -------------------------------------------------------------------------------- | | ViewerMessage / HostMessage / AxisMessage | The protocol | | ViewerGraph | { state: GraphState, options: CalculatorOptions }, what setGraph carries | | GraphReading | { state, options } read back off the calculator, as graphChanged carries it | | ViewerTransport / HostTransport | The two ends of a connection | | createLocalChannel() | An in-process channel; returns { host, viewer } | | createHttpTransport(options?) | The SSE + POST transport, for a viewer served over HTTP | | HttpTransportOptions | { token?, file?, origin?, reconnectGraceMs? }, all defaulted from the page URL | | ConnectionState | 'connecting' \| 'connected' \| 'disconnected' | | PREVIEW_PATHS / PREVIEW_QUERY | The preview server's routes and query keys |

MIT