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

@simpleworkjs/frontend

v0.4.2

Published

Browser-side JavaScript package for SimpleWorkJS apps

Readme

@simpleworkjs/frontend

Browser-side JavaScript for SimpleWorkJS apps. It turns the backend's auto-generated REST API and OPTIONS schema endpoints into live, rendered UI with almost no per-page code.

Full API reference: docs/app.md documents the entire app object — every namespace, method, property, event topic, and data-sw-* attribute. This README is the quick tour.

What it provides

Everything hangs off a global app object:

| Namespace | Purpose | |-----------|---------| | app.api | jQuery AJAX wrapper for the REST API. | | app.model | Schema-aware model client (list/get/create/update/remove) with a schema cache. | | app.pubsub | In-browser publish/subscribe bus (regex topic matching). | | app.socket | Socket.IO client, wired into app.pubsub for live model events. | | app.sync | Live updates: bridges incoming model:* events into refresh events, and binds a jq-repeat scope to a model with app.sync.bind(). | | app.filter | Search/facet filtering for a jq-repeat scope, client-side or server-side. | | app.notify | Notification bell, feed and desktop notifications, driven by the same model events. | | app.render | Builds Bootstrap tables, cards, and forms from the schema. | | app.messages | Contextual action messages, confirm dialogs, and toasts. | | app.util | Small helpers (escapeHtml, formToObject, capitalize, uuid). | | app.ready(cb) | Run a callback once the DOM and socket are ready. | | $.fn.validate / $.validateSettings | [validate] attribute-driven client-side form validation (mirrors, doesn't replace, server-side checks). |

Loading the assets

When you use @simpleworkjs/backend, these files are served for you at /lib/js/, so include them directly:

<script src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://unpkg.com/[email protected]/mustache.min.js"></script>
<script src="/lib/js/app.js"></script>
<script src="/lib/js/app.model.js"></script>
<script src="/lib/js/app.sync.js"></script>
<script src="/lib/js/app.filter.js"></script>
<script src="/lib/js/app.notify.js"></script>
<script src="/lib/js/app.render.js"></script>
<script src="/lib/js/app.custom.js"></script>
<script src="/lib/js/app.validate.js"></script>

You can also resolve asset paths from Node (e.g. to bundle them yourself):

const frontend = require('@simpleworkjs/frontend');
console.log(frontend.assets.app); // .../@simpleworkjs/frontend/lib/app.js

app.api

A thin promise-returning ($.ajax) wrapper. Paths are whatever your API mounts (e.g. /api/Task).

app.api.get('/api/Task', {done: false});     // GET, query params
app.api.options('/api/Task');                 // OPTIONS -> schema
app.api.post('/api/Task', {title: 'Buy milk'});
app.api.put('/api/Task/' + id, {done: true});
app.api.del('/api/Task/' + id);               // DELETE  (note: del, not delete)

The delete method is app.api.deldelete is a reserved word, so it is not used as a method name.

app.model

A higher-level client that fetches every model's schema on startup (via the API root + OPTIONS) and caches it. Prefer this over raw app.api when you want schema-aware helpers.

app.model.ready(function () {          // fires once all schemas are loaded
  const schema = app.model.schema('Task');   // cached OPTIONS response

  app.model.list('Task', {done: false});     // -> {results: [...]}
  app.model.get('Task', id);                  // -> {data: {...}}
  app.model.create('Task', {title: 'New'});
  app.model.update('Task', id, {done: true});
  app.model.remove('Task', id);
  app.model.relatedList('Project', id, 'tasks'); // hasMany association
});

Schema shape

The OPTIONS response cached by app.model (and returned by the backend) looks like:

{
  name: 'Task',        // model name
  pk: 'id',            // primary-key field name
  display: {...},      // model-level display hints (name, titleField, ...)
  fields: {            // per-field metadata (this is the field map)
    title: {name: 'title', type: 'string', htmlType: 'text', display: {...}, ...},
    // ...
  },
  paths: {...},        // REST paths
}

app.render

Declaratively render a model into an element with data-sw-* attributes, then call app.render.build():

<div data-sw-model="Task" data-sw-mode="table"></div>
<div data-sw-model="Task" data-sw-mode="card"></div>
<div data-sw-model="Task" data-sw-mode="form" data-sw-pk="..."></div>
app.model.ready(function () {
  app.render.build('[data-sw-model]');
});
  • data-sw-mode="table" — a Bootstrap table with View/Edit/Delete actions.
  • data-sw-mode="card" — a card grid.
  • data-sw-mode="form" — a create/edit form; supply data-sw-pk to edit an existing record.

Rendered tables and forms are bound to live scopes, so they update automatically when app.sync reports a change. All rendered values are HTML-escaped.

app.pubsub and app.socket

app.socket is a Socket.IO client. When the backend emits a model event it is republished onto the pubsub bus under two topics:

  • model:<Model>:<action> — e.g. model:Task:create.
  • model:any — every model event.

Topic patterns are matched as regular expressions:

const sub = app.pubsub.subscribe('model:Task:.*', function (data, topic) {
  console.log(topic, data);
});
sub.remove(); // unsubscribe

app.sync

The sync layer is what makes a page update by itself when anyone — another user, a background job, or this tab — changes a record.

Listening

app.sync.on('Task', 'update', function (data) { /* ... */ });
app.sync.onAny(function (data) { /* every model change */ });

app.sync.bind() — live updates for a hand-written table

app.render's generated views are live automatically. bind() gives the same behaviour to a table you wrote yourself, without adopting the renderer:

app.sync.bind('hosts', 'Host', {
  key: 'host',            // pk field; defaults to the scope's jq-index-key
  parse: hostParseRow,    // server record -> row object
  filter: fn(row),        // optional: drop rows that don't belong in this list
  fetch: fn(pk),          // optional: re-fetch when an event carries no body
  reveal: true,           // scroll to + flash the changed row (default)
  onChange: fn(),         // called after the scope changes
});

It patches the single changed row rather than reloading the list, so scroll position, checkbox selection and open dropdowns survive someone else's edit.

bind() normalizes both event dialects in use — the framework's model:<Model>:<action> with a {model, action, pk, data} payload, and the model:<Model>:<action>:<pk> form with a bare record — and works on either app.pubsub or a plain app.subscribe bus. Returns {unbind()}.

app.filter

Search and facet filtering for a jq-repeat scope.

const filter = app.filter.bind('hosts', {
  input: '#hostSearch',              // search box
  fields: ['host', 'ip', 'domain.provider'],  // nested paths allowed
  facets: {
    ssl: function (row, value) { return value === 'any' || row.is_wildcard === (value === 'wildcard'); },
  },
  count: '#hostCount',               // optional "3 of 40 shown" readout
  threshold: 500,                    // switch to server mode above this
  fetch: function (state) { ... },   // required for server mode
});

filter.set('ssl', 'wildcard');
filter.clear();

The mode is chosen from the data rather than configured per view: a list filters in the browser until it outgrows threshold, then queries the server (debounced, with out-of-order responses discarded). Without a fetch it stays client-side at any size rather than silently filtering nothing.

In client mode non-matching rows are hidden, not removed — clearing the search brings them straight back with no re-fetch.

app.filter.live() — filtering and live updates together

const {filter, sync} = app.filter.live('hosts', 'Host', {
  input: '#hostSearch',
  fields: ['host', 'ip'],
  parse: hostParseRow,
});

Wires the two together so a row arriving over the socket is shown only if it matches the filter that's active right now — and is still there, ready to appear, when the filter is cleared.

app.messages

Contextual UI feedback.

// Contextual action message anchored to an element:
app.messages.action('Saved successfully', $form, 'success');

// Confirm a destructive action (resolves to a boolean):
const ok = await app.messages.confirm('Delete this record?', $target, 'danger');
if (ok) await app.api.del('/api/Task/' + id);

// Page-wide toast:
app.messages.toast('Welcome back, admin.', 'info');

app.util

app.util.escapeHtml(userInput);   // always escape before injecting into HTML
app.util.formToObject($form);     // serialize a form to a plain object
app.util.capitalize('task');      // 'Task'
app.util.uuid();                  // v4 UUID

$.fn.validate

Attribute-driven client-side form validation, mirroring (not replacing) your server-side checks:

<form>
  <div class="form-group">
    <input name="password" validate="password">
    <b class="invalid-feedback"></b>
  </div>
  <div class="form-group">
    <input name="confirm" validate="eq:password">
    <b class="invalid-feedback"></b>
  </div>
</form>
$('form').on('submit', function(event){
  if (!$(this).validate(event)) return; // event.preventDefault() already called
});
// or: $.validateInit(); // auto-wires every <form action="..."> on submit

Built-in rules: eq:<fieldName> (must match another field), user (uid-style identifier), password (length/character-class policy), ip (dotted-quad). Register your own with $.validateSettings:

$.validateSettings({
  rule: {
    hostname: function(value){
      if (!/^[a-z0-9.-]+$/i.test(value)) return 'Enter a valid hostname';
    }
  }
});

A rule function returns a falsy value when the field is valid, or a message string when it isn't; the message is written into the nearest b.invalid-feedback and the field gets is-invalid/is-valid.

License

MIT

app.notify

Notifications, without a notification system.

A notification system's hard problem is "who should see this" — and the server's socket read gate already answers it, live, per row, every time an event goes out. So there is no recipient resolution and no fan-out: a notification is an event that reached you, and history is those same events replayed through the same gate.

app.notify.configure({
  links: {
    Host: (pk) => '/hosts/' + encodeURIComponent(pk),
    Permission: () => '/permissions',
  },
});

Everything is optional:

| option | default | | |---|---|---| | endpoint | 'activity' | feed + watermark path, relative to app.api's base | | links | {} | model -> (pk) => url; a model with no entry renders unlinked | | collapseWindowMs | 60000 | how long same-model+action events collapse together | | maxRows | 30 | rendered rows; history keeps everything the server returns |

Markup

Binds by id, so the shell owns the styling:

<div id="notify-bell" style="display:none">
  <span id="notify-badge" style="display:none">0</span>
  <a id="notify-desktop-toggle"></a>
  <ul id="notify-list"></ul>
</div>

The bell reveals itself once the feed loads — that proves both a session and the endpoint, and avoids depending on an app-specific "logged in" CSS class.

What the server must expose

GET  <endpoint>       -> {results: [{model, action, target, actor, created_on}], unread, seen_at}
PUT  <endpoint>/seen  <- {seen_at}

Store the shape of each event, not its payload — model, action, pk, actor, timestamp is all the feed renders, and not storing bodies means history never becomes a second copy of your data retaining a deleted record's contents. Filter the replay through the same read gate that decided live delivery.

"Unread" is one watermark per user rather than a read flag per item, so opening the bell on one device clears the badge on all of them.

Collapsing

One user action commonly writes several records — creating a resource in a directory app emits eleven events, and a bulk import emits hundreds. The feed groups same-model+action events inside collapseWindowMs and says "203 resources updated"; history keeps every row.

Desktop notifications

Uses the Web Notifications API, and stays out of the way: permission is only ever requested from a click on #notify-desktop-toggle, and nothing fires while the tab is focused — you are already looking at the page that just updated itself. Repeats of the same model+action reuse the notification tag, so a burst replaces rather than stacks.