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

@evgkch/machjs-inspector

v0.0.8

Published

Look at a state machine: three projections of its transition relation, drawn from the graph and driven by two clicks. Reads a dumped schema, or attaches to a machine that is running

Readme

English · Русский

machjs inspector

Инспектор конечных автоматов на @evgkch/machjs. Читает дамп схемы или подключается к работающей машине и показывает три проекции одного автомата: текст правил, фигуру переходов и прогон. Проекции связаны: наводим на ячейку фигуры — подсвечивается строка правила, наводим на строку — ячейка, называем обе половины перехода — он выполняется.

JSON.stringify(machine) возвращает граф — схему без тел функций, но с их именами. По нему машину рисуют, проверяют и прогоняют.

Открыть инспектор


Содержание

| Раздел | О чём | | -------------------------------------------------------- | ----------------------------------------------------- | | Установка | Точки входа, peer-зависимости | | Способы работы | Страница, работающий автомат, встраивание | | @evgkch/machjs-inspector | inspect, close, RELAY, что идёт по проводу | | Реле | scripts/relay.mjs, порт | | @evgkch/machjs-inspector/ui | overlay, mount, ensemble, субъекты, фокус | | Виджеты | Шесть custom elements и их wiring | | Схемы | Готовые файлы, язык правил | | Ограничения | Что важно помнить при использовании |


Установка

Страницы инспектора и реле — WebSocket-сервер, через который отлаживаемое приложение передаёт свои машины странице наблюдения, — запускаются из клона репозитория. В отлаживаемое приложение ставится npm-пакет:

npm i -D @evgkch/machjs-inspector

В продакшен-сборку вызов inspect не берут:

if (import.meta.env.DEV) inspect(fsm, { name: "cart" });

Если виджеты — часть интерфейса страницы, зависимость обычная.

Пакет поставляется только в формате ESM. Собственная зависимость одна — lit, база custom elements. Peer-зависимости — @evgkch/machjs >= 0.2.1 и @evgkch/chanjs >= 1.1.0: в сборку пакета они не входят, и копия machjs у приложения должна быть одна (см. Ограничения).

Точки входа:

| Точка входа | Что в ней | | -------------- | --------------------------------------------------------------------------- | | . | inspect, close, RELAY. Ни DOM, ни стилей — подходит серверу и воркеру | | ./ui | overlay, mount, виджеты, субъекты. Импорт подключает стили | | ./style.css | Стили светлого DOM: сетка mount и панель overlay | | ./tokens.css | Палитра и типографика; виджетам обязателен |


Способы работы

Готовая страница. В клоне репозитория:

npm install
npm run dev

Откроется инспектор: редактор схемы рядом с фигурой. Схему читают из файла — дамп JSON.stringify(machine) или текст на языке правил, — пишут в редакторе или берут из готовых. Кнопка дампа выписывает набранное обратно в JSON.

Работающий автомат. Реле и страница наблюдения запускаются в клоне репозитория:

npm run inspect      # реле ws://localhost:8999 и страница наблюдения

В отлаживаемое приложение добавляется одна строка:

import { inspect } from "@evgkch/machjs-inspector";

const cart = inspect(new StateMachine(schema, start), { name: "cart" });

На странице появляется cart: его схема, текущее состояние и каждый новый переход. Машин может быть несколько — из одного процесса или из разных.

На своей странице. Инспектор встраивается в отлаживаемую страницу — без реле и второй страницы: целиком через overlay или по одному виджету (см. Виджеты):

import { overlay } from "@evgkch/machjs-inspector/ui";
import "@evgkch/machjs-inspector/style.css";

const gone = overlay(cart);
// …
gone.close();

@evgkch/machjs-inspector

Главная точка входа — то, что пишет приложение. Ни документа, ни стилей: у отлаживаемого процесса может не быть DOM.

inspect

function inspect<T extends AnyMachine>(fsm: T, opts?: Options): T;

inspect возвращает ту же машину, что получил, — тот же объект с одним слушателем на шине. Вызов оборачивают вокруг готового экземпляра в строке, где тот объявлен, а после отладки стирают.

| Поле опций | Что делает | | -------------- | -------------------------------------------------------------------- | | name? | Имя на странице инспектора. Без него — machine N | | description? | Строка о том, для чего машина | | history? | Рекордер history(fsm) из @evgkch/machjs/debug — включает отмотку | | carry? | Слать с шагами payload и контекст — то, что запишет JSON | | url? | Адрес реле, когда оно не на этом хосте. По умолчанию RELAY | | link? | Собственный канал вместо сокета; тогда url не используется |

Машины одного процесса делят один сокет на адрес: соединение открывается на первом inspect и закрывается после ухода последней машины. На pagehide каждая машина шлёт bye.

Отмотка

import { history } from "@evgkch/machjs/debug";

const past = history(cart);
inspect(cart, { name: "cart", history: past });

Отмотка из окна инспектора двигает машину в её процессе через переданный рекордер. Без history в опциях кнопки отмотки на странице не действуют.

Что идёт по проводу

Только имена: граф — схема, как её пишет JSON.stringify, — и четыре типа каждого перехода (from, on, to, emit). Контекст состояний и payload событий не покидают процесс приложения. С carry: true в шаге ещё payload, достигнутый контекст и payload выходного события — в той мере, в какой их запишет JSON; контексту с несериализуемым содержимым задаётся свой toJSON. Эти данные видны в консоли разработчика на странице инспектора.

| Сообщение | Кто шлёт | Содержимое | | --------- | ---------- | ---------------------------------------------------------- | | hello | приложение | Всё о машине: имя, граф, состояние, прогон, возможности | | step | приложение | Один переход и текущее состояние машины | | bye | приложение | Машина перестала публиковаться | | hail | страница | Запрос повторного hello от всех машин | | jump | страница | Команда рекордеру: перейти к записи |

hello содержит состояние целиком, поэтому страница, открытая позже или пропустившая сообщения, шлёт hail и получает всё. Порядок доставки не важен, реле ничего не хранит.

close

function close(): void;

Шлёт bye за все машины и закрывает сокеты. Node-скрипту он нужен: открытый сокет держит event loop, и без вызова процесс не завершится. Вкладке браузера не нужен.

Сигнатуры

function inspect<T extends AnyMachine>(fsm: T, opts?: Options): T;
function close(): void;
const RELAY = "ws://localhost:8999";

type Options = {
  name?: string;
  description?: string;
  history?: Past;
  carry?: boolean;
  url?: string;
  link?: Link;
};

/** Рекордер по форме — подходит `History` из `machjs/debug`. */
type Past = {
  readonly index: number;
  jump(index: number): boolean;
  readonly rx: { on(msg: "moved", hear: (index: number) => void): () => boolean };
};

Реле

Реле — WebSocket-сервер (scripts/relay.mjs). К нему подключаются приложение с машинами и страница наблюдения; напрямую они не связаны.

npm run inspect            # реле и страница наблюдения вместе
npm run relay              # только реле
node scripts/relay.mjs 9001   # свой порт; иначе PORT, иначе 8999

Реле раздаёт каждое сообщение всем остальным клиентам; оно не разбирает сообщений и ничего не хранит. Если порт уже занят другим реле, второй экземпляр печатает одну строку и выходит.


@evgkch/machjs-inspector/ui

Вторая точка входа — интерфейс инспектора для встраивания. Импорт модуля подключает tokens.css.

overlay

function overlay(fsm: AnyMachine, options?: OverlayOptions): Overlaid;

Показывает работающую машину без реле и без второй страницы: фигура и прогон монтируются на этой же странице.

| Поле опций | Что делает | | ------------ | -------------------------------------------------------------- | | into? | Куда монтировать. Без него — плавающая панель поверх страницы | | title? | Подпись в шапке панели | | history? | Рекордер — как у inspect, включает отмотку | | focus? | Общий фокус с чем-то ещё на странице |

Плавающая панель перетаскивается за шапку и закрывается крестиком; Overlaid.close() делает то же из кода и снимает слушателя с машины.

mount

function mount(host: HTMLElement, subject: Subject, options?: ViewOptions): Handle;

Собирает в переданном элементе фигуру и прогон и связывает их: прогон листается с клавиатуры (/, Home/End, Escape), названное правило выполняется, панели располагаются рядом или столбиком по ширине хоста. ViewOptions — тот же focus.

type Handle = {
  readonly update: () => void; // перерисовать: субъект изменился
  readonly enroll: (s: Member) => void; // зарегистрировать ещё один виджет
  readonly fire: (id: RuleId) => void; // выполнить правило, если оно доступно
  readonly destroy: () => void; // отпустить слушателей и DOM
};

/** Виджет, зарегистрированный рядом с парой mount: перерисовывается вместе с ней. */
type Surface = { draw(start: string): void; dress(): void };
/** Виджет со свойством `wiring` проводится при регистрации сам. */
type Member = Surface & { wiring?: { subject: Subject; focus: Focus } };

enroll регистрирует виджет на тех же субъекте и фокусе — например <machjs-diagram>; отдельная проводка странице не нужна.

ensemble

function ensemble(subject: Subject, cast: Cast, options?: { focus?: Focus; start?: string }): Ensemble;

type Cast = { figure?: MachjsFigure; history?: MachjsHistory; diagram?: MachjsDiagram };
type Ensemble = {
  readonly focus: Focus;
  readonly start: string; // от какого состояния считаются ряды
  readonly draw: () => void; // перерисовать всех участников
  readonly dress: () => void; // обновить подсветку
  readonly fire: (id: RuleId) => void; // выполнить правило, если оно доступно
  readonly rewind: (step: number) => void; // подвинуть рекордер, сбросить выбор
  readonly forget: () => void; // сбросить выбор и указатель
  readonly enroll: (s: Member) => void;
  readonly destroy: () => void;
};

Связка без разметки. Виджеты независимы: каждый подписан на субъект и рисует себя; ensemble проводит участников на общие субъект и фокус и выполняет названное на любом из них правило — один раз, в одном месте. mount построен на ensemble и добавляет сетку, измерение и клавиатуру; страница со своей разметкой вызывает ensemble напрямую.

Субъекты

Всё рисуется с субъекта — интерфейса «граф, где стоим, что происходило, как подвинуть»:

function fromMachine(fsm: AnyMachine, opts?: { history?: Past }): Subject;
function fromText(graph: Graph, start: string): Text;

fromMachine читает живую машину. Нажать правило — отправить событие; какое из правил ячейки сработает, определяют условия в приложении.

fromText строит из дампа настоящую машину. Дамп хранит имена условий без кода, поэтому каждому правилу задаётся условие «названо ли именно оно» — названное правило срабатывает.

Фокус

function newFocus(): Focus; // что выбрано и на что наведено — две машины и look()

Конечные автоматы на той же библиотеке. Общий focus на странице — общая подсветка.


Виджеты

Панели инспектора публикуются как custom elements. Импорт любого из них регистрирует элемент:

import {
  MachjsFigure,
  MachjsHistory,
  MachjsEditor,
  MachjsDiagram,
  MachjsLegend,
} from "@evgkch/machjs-inspector/ui";

| Элемент | Класс | Что рисует | | ----------------- | -------------- | ------------------------------------------------------ | | <machjs-figure> | MachjsFigure | Фигуру переходов: три блока вокруг двух осей | | <machjs-history> | MachjsHistory | Прогон: шаги по строкам состояний, отмотка кликом | | <machjs-editor> | MachjsEditor | Текст правил с подсветкой, гуттером и дополнением | | <machjs-diagram> | MachjsDiagram | Классическую диаграмму: состояния в строку, переходы дугами | | <machjs-legend> | MachjsLegend | Строку капсул без рамки: kindstates, in или out | | <machjs-desk> | MachjsDesk | Пульт: меню переключателей и синхронизация остальных |

Виджет настраивают свойством wiring — объектом JavaScript, не атрибутом. Рисует он в shadow root со своим стилем; стили страницы внутрь не проникают. Снаружи читается палитра: токены — custom properties, они наследуются сквозь shadow, поэтому tokens.css обязателен, а переопределение токена меняет и виджет.

Минимальная сборка фигуры с прогоном:

import {
  MachjsFigure,
  MachjsHistory,
  fromMachine,
  newFocus,
} from "@evgkch/machjs-inspector/ui";
import "@evgkch/machjs-inspector/tokens.css";

const subject = fromMachine(fsm);
const focus = newFocus();
const forget = () => {
  focus.choice.dispatch("drop");
  focus.pointer.dispatch("leave");
};
const start = subject.at;

const figure = new MachjsFigure();
figure.wiring = { subject, focus, forget };
host.append(figure);
figure.draw(start); // до draw ничего не нарисовано

const run = new MachjsHistory();
run.wiring = { subject, focus, rewind: (i) => subject.rewind?.(i) };
host.append(run);
run.show(subject.graph, start); // порядок строк
run.draw();

draw зовут снова, когда сменился граф; dress — когда сдвинулся только фокус. Названное правило выполняет не одиночный виджет, а связка — mount или ensemble. Если свой порядок панелей не нужен, используйте mount.

<machjs-diagram> подключается так же: wiring = { subject, focus, fire? }, затем draw(start) — или handle.enroll(diagram). fire берёт правило и сбрасывает выбор; ensemble передаёт свой, без него виджет делает это сам. Состояния — ячейки в строку, переходы — дуги: налево — над строкой, направо — под ней, цвет дуги — цвет целевого состояния. Правила, различающиеся только условием, — одна стрелка; правило с другим emit — своя линия. Подпись — on · when / emit в той мере, в какой у правила есть условие и выход: три down из одного состояния различимы по именам условий. Наведение на дугу подсвечивает её правило в фигуре и в тексте; клик по дуге выполняет доступное правило и сбрасывает выбор. Взятый машиной переход пробегает пунктиром по своей дуге — на каждом шаге, чей бы он ни был. Подпись on / emit действует наравне со своей дугой — и на наведение, и на клик. Клик по состоянию — нажатие в общем выборе: на диаграмме остаются только его переходы, в фигуре — полоса его строки, в истории — пунктирные варианты шага; сбрасывается по Escape и вместе с остальными. Нажатие исходящего состояния и затем входящего берёт переход между ними — второй способ взятия, равный клику по дуге; то же состояние дважды — переход в себя. Наведение на входящее при нажатом исходящем рисует в фигуре полосы кандидатов: строку источника, колонку цели, колонки их событий и строки выходов.

<machjs-legend> — строка капсул без рамки и без управления: атрибут kind выбирает states (имена в цветах полос, текущее залито своим цветом, недостижимые зачёркнуты), in или out. Страницы инспектора держат три такие строки над панелями, каждая под своим переключателем.

<machjs-desk> — пульт: виджет, который управляет остальными. Внутри — свой ensemble, в shadow root — меню: по переключателю на каждый зарегистрированный виджет. Виджеты остаются в разметке страницы; переключатель ставит и снимает hidden.

import { MachjsDesk, MachjsDiagram, fromMachine } from "@evgkch/machjs-inspector/ui";
import "@evgkch/machjs-inspector/tokens.css";

const desk = new MachjsDesk();
desk.wiring = { subject: fromMachine(fsm) };
bar.append(desk);

const diagram = new MachjsDiagram();
host.append(diagram);
desk.enroll(diagram); // проводка, отрисовка и переключатель

Имя переключателя — тег без machjs-; нескольким виджетам одного тега имя задаётся вторым аргументом. desk.seat(имя, { locked?, title? }) — переключатель без проводки, для панели, которую страница показывает и прячет сама; его состояние читается с desk.panels — машины панелей. desk.ensemble — связка (fire, rewind, forget, draw). Меню обеих страниц инспектора — этот же пульт.

<machjs-editor> собирается из readSchema: она возвращает то, что принимает show, — правила с номером строки, цвет каждого состояния и результат проверок.

function readSchema(text: string, keep?: string): Read;

type Read =
  | { ok: true; graph: Graph; start: string; rules: readonly Written[] }
  | { ok: false; say: string; line: number | null };
import {
  MachjsEditor,
  flaws,
  newFocus,
  palette,
  readSchema,
} from "@evgkch/machjs-inspector/ui";
import "@evgkch/machjs-inspector/tokens.css";

const editor = new MachjsEditor();
// За текстом нет машины: выполнять нечего, текущего состояния нет.
editor.wiring = {
  focus: newFocus(),
  onEdit: () => paint(),
  fires: () => false,
  here: () => "",
  fire: () => {},
};
host.append(editor);

let start = "";

function paint(): void {
  const read = readSchema(editor.text(), start);
  // Сообщение и его строка — на нижней полосе редактора.
  if (!read.ok) return editor.blame(read.say, read.line);
  start = read.start;
  editor.blame(null, null);
  editor.show(read.rules, palette(read.graph, start), flaws(read.graph, start));
}

editor.set("FROM locked ON coin TO open\nFROM open ON pass TO locked\n");
paint();

readSchema читает оба вида текста — язык правил и JSON-дамп — и не бросает исключений. ok — признак разбора: при true возвращаются graph, start и rules, при false — сообщение say и строка line, к которой оно относится; у дампа строк нет, и там line равно null. Второй аргумент — состояние, с которого продолжается прогон: оно сохраняется, пока такое состояние есть в графе, иначе берётся первое состояние графа. onEdit вызывается на каждое нажатие клавиши — страница инспектора ждёт 300 мс и только потом читает текст. fires возвращает, доступно ли правило из текущего состояния, here — имя этого состояния, fire выполняет правило: если передать subject и общий с фигурой focus, клик по метке в гуттере выполняет правило своей строки. Типы экспортируются оттуда же: Read, Shown, Written, Row, Lane и Flaws.


Схемы

В schemas/ лежат девять готовых файлов. Шесть — the-inspectors-* — схемы машин самого инспектора, выписанные из его исходников командой npm run dump. Три написаны вручную: selection-rectangle, upload-with-retry и a-schema-with-problems — последняя показывает, как выглядят мёртвые правила и недостижимые состояния.

Файл читается в двух видах: JSON-дамп и язык правил библиотеки —

FROM locked ON coin WHEN underCap TO locked WITH addCoin
FROM locked ON coin               TO open   WITH reset   EMIT opened
FROM open   ON pass               TO locked

FROM, ON и TO обязательны, порядок слов фиксирован, комментарии — # или // до конца строки. После круга «текст → схема → дамп» получается та же схема, а не эквивалентная.


Ограничения

  • Копия @evgkch/machjs у приложения должна быть одна. Переходы публикуются по символу TRANSITION, а у второй копии библиотеки символ свой — слушатель inspect не срабатывает. При сборке из нескольких бандлов помогает resolve.dedupe у Vite.
  • Node-скрипту нужен close(). Открытый сокет держит event loop; без вызова процесс не завершится.
  • Отмотка требует переданного рекордера. Без history в опциях inspect кнопки отмотки не действуют.
  • По проводу идут только имена, если не сказано carry. Условия дампа не выполняются: страница наблюдения отправляет событие, а какое правило сработает, определяют условия в процессе приложения. Без carry: true контекст и payload не покидают приложение.
  • Виджетам нужен tokens.css. Палитра проходит в shadow root через custom properties; без токенов виджет остаётся без цветов.

Лицензия

MIT