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

@12-apps/app-shell

v5.7.0

Published

The shell three SPAs of one product share (12-18). Framework-free core (.): the typed JSON API client and its ApiError, the WCAG brand-palette correction, pt-BR formatting, the stale-chunk recovery and the consent wire. Browser half (./react: createWebApp

Readme

@12-apps/app-shell

The shell several SPAs of one product share, as an installable package: the typed API client and its ApiError, the MUI theme built from a tenant's brand seed, the session, the route error boundary, route-level code splitting that survives a deploy, the terms/privacy consent gate — and the small backend surface that gate needs.

pnpm add @12-apps/app-shell

Adoption contract, the required knobs and the sharp edges: ADOPTING.md.

Why it is a package

Three SPAs cannot share nothing, so they share a private package — and a private package is one nobody else can install. This one was 5218 LOC of exactly the things every multi-tenant product needs in a browser, and its subsystems have been going out to their own packages one at a time (@12-apps/realtime, @12-apps/notifications, @12-apps/observability-frontend, @12-apps/auth). What remained is the shell itself, and it is what all of them mount inside.

The two halves

const shell        = createWebAppShell({ /* config */ });   // browser
const { routes }   = createApiAppShell({ /* config */ });    // backend

shell.Provider around your routes is the whole browser wiring — the route error boundary is inside it, so onCrash is reached without you mounting anything. Place a second boundary below your own chrome if you want a crashed page to keep the sidebar; the nearest one catches, so nothing doubles.

| Subpath | What is in it | |---|---| | . | Framework-free: apiFetch / ApiError, restResult, joinApiPath / stripTrailingSlashes, the WCAG brand-palette correction, pt-BR money and duration formatters, the stale-chunk recovery, the consent wire. | | ./react | createWebAppShell — the provider tower (boundary included), the theme, lazyRoute, the consent gate, useDeviceDetection, useServerDataViews, and the collapsible nav's persisted state (useCollapsedSections, useSidebarRail). | | ./server | createApiAppShell — the consent status/accept descriptors, framework-neutral. | | ./hono | The forty-line adapter. hono is an optional peer. | | ./vite | appShellOptimizeDeps() — the dependency pre-bundling preset. Compiled, and it has to be. |

Two clients, and the split is by CALLER

apiFetch throws ApiError on a non-2xx. restResult returns { ok: false, error, fieldErrors?, status? }. Both speak the { data } / { error, issues } envelope every createApi* package answers in, so a screen can mix them without a second adapter.

Reach for apiFetch for reads and for anything whose failure is somebody else's to handle — a query, a prefetch, a background refresh. Reach for restResult for a write a human is waiting on: a submit has two ordinary outcomes, one of which is "the server refused it and named the fields", so writing that as a throw costs every handler a try/catch whose catch is the main path.

status is deliberately ABSENT on a network failure. That is the only thing separating "the server said no" from "nothing answered" — one is a message to show, the other is a retry — and it is what lets a concurrent surface tell a 403 ("not yours", say so beside the button) from a 409 ("somebody was faster", refresh).

useServerDataViews — the URL is the query

A DataViewsGrid in server mode filters and sorts nothing itself: it EMITS a DataViewQuery and the host fetches. This hook is the router-driven answer — it maps that query into search params and replaces them, and the page's query hook, keyed on those params, re-fetches. Keeping the state in the URL is most of what separates an admin list somebody can work in from one they fight: it makes a filtered view linkable, reloadable and back-button-able.

const server = useServerDataViews({
  totalCount, page, pageSize,
  toParams: (query) => ({ q: query.search || undefined, type: query.filters.type }),
});

Two behaviours are load-bearing and are pinned by cases rather than left to comments:

  • Only free text debounces. A filter chip or a page number leaves the search unchanged and applies synchronously; a click that waited 250ms feels broken.
  • It merges against the LIVE URL, not the router's functional snapshot. A debounced search commits from a timer scheduled before an interleaving write by another owner of the URL (a row click writing ?view=), and the router's snapshot can lag it — so the row selection would vanish a moment after the operator made it.

The basename is read from the ROUTER (useHref('/')), not from the bundler's configured base. Those are two sources for one fact that agree only while they are configured identically, and reading the bundler's would make the module unloadable outside it.

Three things worth knowing before you read the code

ApiError is load-bearing for another published package. @12-apps/entitlements' upsell channel decides whether a rejection is a plan denial by reading status and body off one of these. The three fields are pinned by a test for that reason.

Nothing here reports success it cannot back. The consent endpoint propagates a failed write as a 500 rather than answering 204, because a 204 over a failed write tells the user they accepted while every guard keeps refusing them. onCrash, isCurrent, consent and brand.name are required rather than defaulted, because each of their plausible defaults fails silently in exactly the direction nobody checks — and the boundary onCrash is reported from is mounted rather than documented, for the same reason.

A colour a tenant TYPED is not a colour you can paint. A brand hex is chosen to look good on a sign, not to be legible as 14px text on a white card: one real seeded tenant's #7ED957 renders its prices at 1.76:1 against a 4.5:1 floor. The palette keeps the hue and moves only the lightness, so the tenant recognises their colour and cannot pick an unreadable one.

The consent story, in one paragraph

POST /consent/terms could always FIX a user whose acceptance had gone stale. Nothing could TELL them, so nobody ever called it: bumping the terms version turned every previously-consented user into a pending one silently — still signed in, avatar and cart intact — and the first thing they heard was a bare 401 {"error":"Unauthorized"} at the payment step, with a retry that could never succeed. GET /consent/status is the missing half and the gate is what renders it, which is why both halves are in one package: the two ends agreeing on a path is the whole feature.

Tests

pnpm test          # the package's own suites

The consumer proof lives in harness/ at the repo root: a page driving the real createWebAppShell against a real createApiAppShell mount, both installed from packed tarballs.