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

@nseaprotector/acme-script

v0.4.0

Published

Elixir-flavored JS helpers for LiveView hooks and .heex templates

Readme

AcmeScript

CI

Client-side JavaScript for Elixir/Phoenix projects. Brings helpers close to Elixir idioms (pipe, ok/error, match, with, Enum, immutable access on nested maps) to simplify writing LiveView hooks and .heex templates.

No build step required: native ES modules, importable directly in the browser or through your Phoenix asset bundler (esbuild/vite). Ships hand-written .d.ts typings (acmescript.d.ts, hooks/index.d.ts), so plain JS consumers get autocomplete/IDE hints with no extra setup.

pnpm add @nseaprotector/acme-script
import { pipe, ok, error, match, cond, unless, inspect, H, J, find, show, hide,
         createHook, createLiveComponent, Enum, getIn, putIn, updateIn,
         PubSub, withDo } from "./acmescript.js";

Structure

acmescript.js   public entry point (re-exports src/index.js)
acmescript.d.ts typings for the entry point (hand-written, checked via `pnpm typecheck`)
src/
  core.js            pipe, ok, error, match, cond, unless, inspect
  sigils.js          H (HTML template), J (safe JSON)
  dom.js             find (fluent DOM selection)
  transitions.js     transition, show, hide
  hook.js            createHook (LiveView hook wrapper)
  live_component.js  createLiveComponent (stateful custom element)
  enum.js            Enum (map/filter/reduce/... Elixir-style)
  access.js          getIn, putIn, updateIn (immutable access)
  pubsub.js          PubSub (client-side, independent of the server)
  with.js            withDo (Elixir `with`-style chaining)
hooks/          classic LiveView hooks built on top of the lib, see below
demo/           a Phoenix app demoing every hook, see below

API

pipe(value, ...fns)

Chains unary functions, |>-style.

pipe(5, (x) => x + 1, (x) => x * 2); // 12

ok(data) / error(err) / match(result, clauses)

Typed result {ok, data, error} (also iterable as [bool, data]), {:ok, _} / {:error, _}-style.

const result = ok({ id: 1 });
match(result, {
  ok: (data) => console.log("found", data),
  error: (err) => console.error(err)
});

cond(pairs)

Evaluates [test, resultOrFn] pairs in order, cond do block-style. resultOrFn can be a plain value or a zero-arg function (called lazily, only for the matching branch).

cond([
  [score > 90, "A"],
  [score > 70, () => computeGrade(score)],
  [true, "F"]
]);

Two differences from Elixir's cond do: pairs is a plain array, so every test expression is evaluated eagerly before cond() runs (only the matching branch is lazy). And because a function branch is called rather than returned, wrap it (() => myFn) if you actually need the function itself as the result.

unless(test, branch)

Runs branch when test is falsy, the inverse of if.

unless(user.isActive, () => deactivate(user));

inspect(label)

IO.inspect-style: logs the value (with an optional label) and passes it through unchanged. Meant to be dropped into a pipe chain for debugging.

pipe(input, inspect("before"), transform, inspect("after"));

H`...` — HTML sigil

Parses a template literal into an HTMLElement or DocumentFragment.

const el = H`<div class="card">${title}</div>`;
document.body.append(el);

J`...` — safe JSON sigil

Parses a JSON template literal, returns ok/error (never throws).

const [success, data] = J`${jsonString}`;
if (success) render(data);

find(selector, parent = document)

Selects an element and returns a fluent wrapper (ok/error + chainable methods).

find("#modal")
  .addClass("open")
  .attr("aria-hidden", "false")
  .on("click", (e) => console.log(e));

transition(target, opts) / show(target, opts) / hide(target, opts)

Class-based CSS transitions (Tailwind-compatible), inspired by Phoenix LiveView's JS.show/hide.

show("#modal");
hide("#modal", { duration: 200 });

createHook(spec)

LiveView hook wrapper: injects a ctx (push, pushTo, handle, upload) into mounted/updated/destroyed.

export default createHook({
  mounted(ctx) {
    ctx.handle("refresh", (payload) => this.el.textContent = payload.value);
    ctx.push("ready");
  }
});

createLiveComponent({ mount, handleEvent, render })

Standalone custom element with local state, hydrated from phx-state.

customElements.define("acme-counter", createLiveComponent({
  mount: (state) => ({ count: state.count ?? 0 }),
  handleEvent: {
    inc: (state) => ({ count: state.count + 1 })
  },
  render: (state, send) => H`<button onclick="${() => send("inc")}">${state.count}</button>`
}));

Enum

List transformation pipeline, Elixir Enum-style. Each function returns a (list) => list (or (list) => value) transformer, to compose with pipe.

map, filter, reject, reduce, take, chunkEvery, uniq, sort, each, any, all, count, find, groupBy, sum.

pipe(
  users,
  Enum.filter((u) => u.active),
  Enum.map((u) => u.name),
  Enum.uniq(),
  Enum.take(10)
);

Enum.groupBy((u) => u.role)(users); // { admin: [...], user: [...] }
Enum.count((u) => u.active)(users); // 3

getIn(obj, path, default) / putIn(obj, path, val) / updateIn(obj, path, fn)

Immutable read/write on nested objects, Kernel.get_in/put_in/update_in-style.

const state = { user: { profile: { name: "Ada" } } };
getIn(state, ["user", "profile", "name"]);          // "Ada"
putIn(state, ["user", "profile", "name"], "Grace");  // new object
updateIn(state, ["user", "profile", "name"], (n) => n.toUpperCase());

PubSub

Client-side pub/sub, independent of the server-side Phoenix.PubSub — useful for letting hooks/components on the page talk to each other.

const unsubscribe = PubSub.subscribe("cart:updated", (payload) => render(payload));
PubSub.broadcast("cart:updated", { count: 3 });
unsubscribe();

withDo(...steps)

Chains steps that return ok(data)/error(err), short-circuits on the first error, Elixir with-style. Each ok(data).data is merged into the accumulated context.

withDo(
  () => validateForm(input) ? ok({ input }) : error("invalid"),
  (ctx) => saveToServer(ctx.input)
);

Examples

See examples/ for complete use cases (LiveView hook, live component, functional composition).

Classic hooks

hooks/ is a small cookbook of common LiveView hooks built with createHook and the rest of the lib: CopyToClipboard, AutoResize, ClickOutside, InfiniteScroll, Reorderable, LocalStorageSync, Hotkey, ScrollRestore, ContextMenu, TagsInput, Dropzone. They ship as a package subpath so you don't need to copy-paste them:

import { CopyToClipboard, ClickOutside } from "@nseaprotector/acme-script/hooks";

const liveSocket = new LiveSocket("/live", Socket, {
  hooks: { CopyToClipboard, ClickOutside }
});

Each hook reads its config from data-* attributes — see the comment at the top of its source file for the expected markup, or the demo below for a working example of every one.

Demo

demo/ is a minimal Phoenix app with one LiveView page per hook (mix phx.new --no-ecto --no-mailer), wired to the local lib via a pnpm workspace so it always tracks the current source, no publish needed:

pnpm install                # from the repo root, links demo/assets to the lib
cd demo
mix setup
mix phx.server

Then open localhost:4000 for the index of every hook demo.

Tests

pnpm test

See test/ (vitest + jsdom, one file per module in src/).

Typings

pnpm typecheck

Checks acmescript.d.ts and hooks/index.d.ts against typecheck/acmescript.test-d.ts, a file that exercises the full public API — the typings equivalent of the test suite, catching declarations that drift from actual usage.

Minified build

pnpm build

Generates dist/acmescript.min.js (ESM, ~3.3kb minified) via esbuild. Not versioned, regenerate it when deploying Phoenix assets.