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

webhanger

v1.1.1

Published

Component-as-a-Service platform — bundle, sign, and deliver UI components via edge CDN

Readme

WebHanger

Component-as-a-Service (CaaS) — Bundle once. AES-256 encrypt. Deploy to edge CDN. Load anywhere with zero code.

WebHanger is a secure, edge-delivered component distribution platform. Deploy encrypted UI components to a CDN and load them into any website or framework with a single tag — no tokens in HTML, no exposed secrets, no configuration.


Packages

| Package | Install | Description | |---|---|---| | webhanger | npm install -g webhanger | CLI + Node.js library | | webhanger-front | npm install webhanger-front | Browser + ESM SDK | | webhanger-admin | npm install webhanger-admin | Admin SDK + dashboard | | webhanger-auth | npm install webhanger-auth | OAuth authentication SDK |


Quick Start

npm install -g webhanger
wh init
wh ship ./components ./site 1.0.0 ./dist

Load in any HTML:

<script src="https://unpkg.com/webhanger-front@latest/browser.min.js"></script>
<script>WebHangerFront.initialize("./wh-manifest.json");</script>

<wh-component name="navbar"></wh-component>
<wh-component name="hero"></wh-component>
<wh-component name="footer" sandbox></wh-component>

Load in Next.js / React / Vite:

import { load } from "webhanger-front";

const manifest = await fetch("/wh-manifest.json").then(r => r.json());
const c = manifest.components["navbar"];
await load(c.urls || c.url, manifest.pid, c.token, c.expires, "#nav-mount");

CLI Reference

wh init

Interactive setup. Provisions S3 bucket + CloudFront automatically. Supports Firebase, Supabase, MongoDB. Optional Cloudflare Edge Worker setup.

wh dev ⭐ Development server

Local dev server with hot reload, live preview, and manifest auto-refresh.

wh dev ./components 4242

Edit any component file → auto-deploys in 300ms → browser hot-reloads. Open http://localhost:4242 for live preview with a dev status bar at the bottom.

# Custom port + manifest output
wh dev ./components 3000 ./public/wh-manifest.json

wh ship

Deploy + build + zip in one shot.

wh ship ./components ./site 1.0.0 ./dist
  1. Deploys all components (bundle → AES-256 encrypt → upload → HMAC sign → register)
  2. Resolves dependency graph
  3. Writes wh-manifest.json
  4. Production builds the site
  5. Zips for upload

wh deploy

wh deploy ./components/navbar navbar 1.0.0

wh graph-deploy

wh graph-deploy ./components 1.0.0 ./output

wh atomize

Split a single HTML page into CDN-powered components.

wh atomize ./docs/index.html ./atomized 1.0.0

wh build

Production build — minifies HTML, extracts CSS/JS to hashed files.

wh build ./site ./dist

wh zip

wh zip ./dist ./deploy.zip

wh analyze

wh analyze ./components/navbar

wh convert

wh convert ./components/navbar navbar react ./output
# targets: react | vue | svelte | next | angular | astro

wh breakdown

Extract embedded CSS/JS from a single HTML file.

wh breakdown ./components/navbar

wh access

wh access grant
wh access revoke <key>
wh access list

wh edge-init

wh edge-init
cd edge && wrangler deploy

wh auth init

Interactive OAuth setup — Google, GitHub, Facebook.

wh auth init

Prompts for providers, base URL, client IDs/secrets. Generates wh-auth.config.json with callback URLs.

wh auth serve

Start the OAuth callback server.

wh auth serve
wh auth serve 3001   # custom port

Handles OAuth redirects, token exchange, JWT issuance.


Component Structure

components/
  navbar/
    index.html
    style.css
    script.js
    webhanger.component.json

webhanger.component.json

{
  "assets": [
    { "type": "script", "url": "https://cdn.tailwindcss.com" }
  ],
  "dependencies": ["[email protected]"],
  "props": {
    "brand":   { "type": "string", "default": "MyApp" },
    "ctaText": { "type": "string", "default": "Get Started" },
    "ctaHref": { "type": "string", "default": "/signup" }
  }
}

Component Props System

Pass dynamic data into components via HTML attributes — no redeployment needed.

In your component HTML, use {{wh.propName}} placeholders:

<!-- index.html -->
<nav>
  <span class="brand">{{wh.brand}}</span>
  <a href="{{wh.ctaHref}}" class="cta">{{wh.ctaText}}</a>
</nav>

Pass props via attributes:

<wh-component
  name="navbar"
  wh-brand="Acme Corp"
  wh-cta-text="Sign Up Free"
  wh-cta-href="/register">
</wh-component>

Or programmatically:

await load(url, pid, token, 0, "#mount", null, [], {
  props: {
    brand: "Acme Corp",
    ctaText: "Sign Up Free",
    ctaHref: "/register"
  }
});

Props are resolved after decryption — the encrypted payload on CDN contains {{wh.brand}} literally. The SDK substitutes values in memory before DOM injection. Defaults from webhanger.component.json are used when no prop is passed.


Security

AES-256-GCM Encryption

Key     = SHA-256(projectId + salt)
Payload = iv:tag:ciphertext  (base64)
Salts   = "::html" | "::css" | "::js"

HMAC-SHA256 Signed URLs

token = HMAC-SHA256(projectId:path:expires, secretKey)

Integrity Check

SHA-256 hash verified after decryption — detects tampering.

Domain Restriction

load(url, pid, token, 0, "[data-wh]", null, [], {
    allowedDomains: ["mysite.com"]
});

Manifest-based Delivery

Tokens, projectId, CDN URLs never in HTML. Fetched at runtime from wh-manifest.json.


Dependency Graph

[email protected]
  ├── [email protected]
  └── [email protected]
        └── [email protected]
import { resolveGraph } from "webhanger";
const graph = await resolveGraph(config.db, projectId, "dashboard", "1.0.0");
// Returns: [chart, navbar, statsbar, dashboard]

Multi-CDN Failover

{
  "cdn": {
    "url": "https://primary.cloudfront.net",
    "fallbacks": ["https://fallback.r2.dev"]
  }
}

Edge Worker (Cloudflare Workers)

  • HMAC token validation at edge
  • Version resolution (latest1.2.0)
  • Geo-based routing
  • Rate limiting (100 req/min per IP)

Edge Personalization

Serve different component variants to different users based on rules — country, device, role, A/B test, subscription plan — all resolved client-side or at the Cloudflare edge.

Setup

# Scaffold rules config
wh personalize init

# Test rule resolution (slot, config, country, device, role)
wh personalize test ./wh-personalization.json IN mobile premium
# Output:
# Context: country=IN device=mobile role=premium
# Resolved: hero → hero-india  (first matching rule: country=IN)

wh-personalization.json

Key rule: The slot key (e.g. "hero") must exactly match the name attribute on <wh-component name="hero">. The SDK uses the component name as the slot lookup key.

{
  "hero": {
    "rules": [
      { "if": { "country": "IN" },          "component": "hero-india"    },
      { "if": { "country": "US" },          "component": "hero-us"       },
      { "if": { "device": "mobile" },       "component": "hero-mobile"   },
      { "if": { "role": "premium" },        "component": "hero-premium"  },
      { "if": { "abTest": "variant-b" },    "component": "hero-variant-b"},
      { "if": { "hour": { "min": 9, "max": 17 } }, "component": "hero-business" }
    ],
    "default": "hero"
  },
  "navbar": {
    "rules": [
      { "if": { "role": "admin" },   "component": "navbar-admin"   },
      { "if": { "plan": "premium" }, "component": "navbar-premium" }
    ],
    "default": "navbar"
  }
}

Zero-code usage

<script src="https://unpkg.com/webhanger-front@latest/browser.min.js"></script>
<script>
  // Load rules first, then initialize
  WebHangerFront.loadPersonalization("./wh-personalization.json");
  WebHangerFront.initialize("./wh-manifest.json");
</script>

<!-- personalize attribute enables rule resolution -->
<wh-component name="hero" personalize></wh-component>
<wh-component name="navbar" personalize></wh-component>

Programmatic

import { load, loadPersonalization } from "webhanger-front";

await loadPersonalization("./wh-personalization.json");

// Override context for testing
window._wh_ctx_override = { country: "IN", role: "premium" };

// Component resolves to hero-india (first matching rule)
await load(manifest.components["hero"].url, pid, token, 0, "#hero");

Rule conditions

| Condition | Source | Example | |---|---|---| | country | window._wh_ctx_override or IP (edge) | "IN", ["IN","PK"] | | device | User-Agent | "mobile", "tablet", "desktop" | | role | JWT from webhanger-auth | "premium", "admin" | | abTest | localStorage bucket (stable per user) | "variant-a", "variant-b" | | lang | navigator.language | "en", "hi" | | hour | time of day | { "min": 9, "max": 17 } | | plan | localStorage wh_plan | "free", "premium" |

Rules are evaluated top-to-bottom — first match wins. Falls back to default if no rule matches.

Events

WebHangerFront.on("personalized", ({ slot, resolved, ctx }) => {
    console.log(`${slot} → ${resolved} (country: ${ctx.country})`);
});

A/B Testing

Each user is automatically assigned a stable bucket (variant-a or variant-b) stored in localStorage. 50/50 split by default.

// Check current bucket
console.log(localStorage.getItem("wh_ab_bucket")); // "variant-a" or "variant-b"

Edge resolution (Cloudflare Workers)

When using wh edge-init, the worker resolves personalization server-side using Cloudflare headers — no client-side JS needed:

CF-IPCountry: IN  →  worker resolves hero-india  →  serves encrypted component

Store rules in KV:

wrangler kv:key put --binding=WH_VERSIONS "personalization:hero" '{"rules":[...],"default":"hero"}'

Test it

node personalization-test/deploy.js
npx serve personalization-test/site

Open http://localhost:3000 — use the simulator buttons to switch country, device, role, and A/B bucket. Watch the hero component change in real time.


Drop-in OAuth for any website. Supports Google, GitHub, Facebook. Zero backend code required beyond wh auth serve.

Setup

npm install -g webhanger        # CLI
npm install webhanger-auth      # browser SDK

wh auth init                    # configure providers
wh auth serve                   # start OAuth server (default port 3001)

Zero-code HTML

<script src="https://unpkg.com/webhanger-auth/browser.min.js"></script>

<!-- Renders a styled OAuth button, handles the full flow -->
<wh-auth
  provider="google"
  on-success="/dashboard?name={{name}}&email={{email}}"
  on-error="/login?error={{message}}"
  theme="dark">
</wh-auth>

<wh-auth provider="github"   on-success="/dashboard"></wh-auth>
<wh-auth provider="facebook" on-success="/dashboard"></wh-auth>

Available on-success template variables: {{name}} {{email}} {{avatar}} {{provider}} {{id}}

Programmatic

// Login
WHAuth.login("google", {
    onSuccess: (user) => {
        console.log(user.email, user.name, user.avatar, user.provider);
        redirect("/dashboard");
    },
    onError: (err) => showError(err.message)
});

// Check session
WHAuth.isLoggedIn();  // true/false
WHAuth.getUser();     // { email, name, avatar, provider, id }
WHAuth.getToken();    // JWT string

// Logout
WHAuth.logout();

// Events
WHAuth.on("success", ({ user, token }) => analytics.track("login", user));
WHAuth.on("error",   ({ message })     => errorTracker.capture(message));
WHAuth.on("logout",  ()                => clearSession());

wh-auth.config.json (generated by wh auth init)

{
  "baseUrl": "https://myapp.com",
  "callbackPath": "/auth/callback",
  "port": 3001,
  "providers": {
    "google": {
      "clientId": "...",
      "clientSecret": "...",
      "callbackUrl": "https://myapp.com/auth/callback/google"
    },
    "github": {
      "clientId": "...",
      "clientSecret": "...",
      "callbackUrl": "https://myapp.com/auth/callback/github"
    }
  },
  "session": {
    "secret": "auto-generated",
    "expiresIn": "7d"
  }
}

Verify JWT server-side

import { verifyAuthToken } from "webhanger-auth";

const user = await verifyAuthToken(req.headers.authorization?.split(" ")[1]);
// { email, name, avatar, provider, id, exp, iat }

Auth flow

User clicks <wh-auth provider="google">
  └── popup opens → /auth/google
        └── redirect to Google OAuth consent
              └── Google → /auth/callback/google?code=xxx
                    └── exchange code → fetch profile
                          └── issue JWT
                                └── postMessage to opener
                                      └── WHAuth.on("success") fires
                                            └── redirect to on-success URL

A local web UI + SDK for managing deployed components.

npm install webhanger-admin

# Start dashboard
npx wh-admin ./webhanger.config.json 5000

Open http://localhost:5000

Or use programmatically:

import { WebHangerAdmin } from "webhanger-admin";

const admin = new WebHangerAdmin("./webhanger.config.json");

const components = await admin.listComponents();
const manifest   = await admin.generateManifest();
await admin.saveManifest("./public/wh-manifest.json");

const { apiKey } = await admin.grantAccess("deployer", "CI/CD");
await admin.resignComponent("navbar", "1.0.0", 86400);
await admin.deleteComponent("navbar", "1.0.0");

See webhanger-admin/README.md for the full API reference.


Browser SDK

Zero-code Custom Element

<script src="https://unpkg.com/webhanger-front@latest/browser.min.js"></script>
<script>WebHangerFront.initialize("./wh-manifest.json");</script>

<wh-component name="navbar" wh-brand="MyApp" wh-cta-text="Start Free"></wh-component>
<wh-component name="footer" sandbox></wh-component>

ESM (Next.js / React / Vite)

import { load, use, on, registerSW, clearCache, metrics, gpu } from "webhanger-front";

Manual load with props

await load(
    cdnUrl,
    projectId,
    token,
    expires,
    selector,
    onSignal,
    deps,
    {
        props: { brand: "MyApp", ctaText: "Get Started" },
        sandbox: true,
        allowedDomains: ["mysite.com"],
        beforeMount: () => showSpinner(),
        afterMount:  () => hideSpinner(),
        onError:     (err) => showFallback(err)
    }
);

Signal callback

load(url, pid, token, 0, "[data-wh]", ({ stage, time, source }) => {
    // stages: start → fetching → assets → deps → injecting → done | error
});

Plugin system

use({
    install({ on }) {
        on("load",   ({ time, source }) => analytics.track("load", { time, source }));
        on("error",  ({ message })      => errorTracker.capture(message));
        on("metric", ({ name, value })  => dashboard.update(name, value));
        on("gpu",    ({ supported })    => console.log("WebGPU:", supported));
        on("sw",     ({ scope })        => console.log("SW:", scope));
    }
});

Observability

on("load", ({ time, source }) => console.log(time, source));
console.log(metrics); // { loads, cacheHits, errors, totalTime }

WebGPU

console.log(gpu.supported);
on("gpu", ({ supported }) => console.log("GPU:", supported));

Offline + Service Worker

await registerSW("./webhanger.sw.js");

await setOfflinePage(
    "<h1>Offline</h1><p>Back soon.</p>",
    "body { background: #030712; color: white; }"
);

Offline behavior:

  • Online first visit → loads from CDN, caches everything
  • Online repeat visit → loads from SW cache instantly, badge shows
  • Offline with cache → full page works
  • Offline no cache → custom offline page with "⬡ Served by WebHanger" badge

Smart Cache Invalidation

Only re-fetches from CDN when components actually changed. Checks admin server on every page load — if nothing changed, loads instantly from cache.

// Instead of initialize(), use smartInitialize()
WebHangerFront.smartInitialize(
    "./wh-manifest.json",
    "http://localhost:5000"  // wh-admin server URL
);

Flow:

Page loads
  └── GET /api/last-updated from admin server (2s timeout)
        ├── same as localStorage "wh_last_updated"
        │     └── load all components from cache (instant, 0ms)
        └── different (components updated since last visit)
              └── clear component cache
                    └── reload from CDN
                          └── update localStorage timestamp

Events:

WebHangerFront.on("cache-invalidated", ({ serverTs, cachedTs }) => {
    console.log("Components updated — reloading from CDN");
});
WebHangerFront.on("cache-hit", ({ serverTs }) => {
    console.log("Up to date — loaded from cache");
});

If admin server is unreachable (offline, not running), falls back to normal cache behavior silently — zero errors.

Test it:

# 1. Deploy a component
node smart-cache-test/deploy.js 1.0.0

# 2. Start admin server
node admin/server.js ./webhanger.config.json 5000

# 3. Serve the test page
npx serve smart-cache-test/site

# 4. Open http://localhost:3000 — loads from CDN, caches timestamp
# 5. Refresh — loads from cache instantly (banner: "✓ Up to date")
# 6. Redeploy with new version
node smart-cache-test/deploy.js 2.0.0
# 7. Refresh — cache invalidates, loads fresh (banner: "🔄 Components updated")

See smart-cache-test/ for the full working demo.

Hard flush

await clearCache();

Caching

| Layer | Used for | |---|---| | localStorage | Components < 50KB | | IndexedDB | Components ≥ 50KB | | Service Worker | Offline + navigation cache | | wh_last_updated | Smart cache invalidation timestamp |

Stale-while-revalidate — returns cached instantly, refreshes in background.

Smart cache invalidationsmartInitialize() checks admin server on every load. Only re-fetches from CDN when components actually changed. Zero CDN requests on cache hits.


Access Control

| Role | deploy | read | delete | manage_access | |---|---|---|---|---| | owner | ✅ | ✅ | ✅ | ✅ | | admin | ✅ | ✅ | ✅ | ✅ | | deployer | ✅ | ✅ | ❌ | ❌ | | viewer | ❌ | ✅ | ❌ | ❌ |


Node.js API

import { WebHanger } from "webhanger";
const wh = new WebHanger();

const result = await wh.deploy("./components/navbar", "navbar", "1.0.0", {
    expiresInSeconds: 86400,
    dependencies: ["[email protected]"]
});

const comp = await wh.resolve("navbar", "1.0.0");
await wh.resign("navbar", "1.0.0", { expiresInSeconds: 3600 });
await wh.remove("navbar", "1.0.0");

Next.js Integration

npm install webhanger-front
"use client";
import { useEffect, useRef } from "react";
import { load } from "webhanger-front";

export default function WebHangerComponent({ name, props = {} }: { name: string; props?: Record<string, string> }) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    fetch("/wh-manifest.json")
      .then(r => r.json())
      .then(m => {
        const c = m.components[name];
        if (!c || !ref.current) return;
        ref.current.id = `wh-${name}`;
        load(c.urls || c.url, m.pid, c.token, c.expires, `#wh-${name}`, null, [], { props });
      });
  }, [name]);

  return <div ref={ref} />;
}
// app/page.tsx
<WebHangerComponent name="navbar" props={{ brand: "MyApp", ctaText: "Sign Up" }} />

Examples & Tests

| Folder | What it tests | |---|---| | examples/ | Full component deploy + browser SDK demo | | showcase/ | All 4 packages together | | auth-test/ | OAuth login flow (Google, GitHub) | | smart-cache-test/ | Smart cache invalidation demo | | personalization-test/ | Edge personalization — country, device, role, A/B |

# Examples
node examples/deploy.js && npx serve examples/site

# Showcase (all packages)
node showcase/deploy.js && npx serve showcase/site

# Auth test
wh auth init && wh auth serve &
npx serve . # open /auth-test/login.html

# Smart cache test
node smart-cache-test/deploy.js 1.0.0
node admin/server.js ./webhanger.config.json 5000 &
npx serve smart-cache-test/site

# Personalization test
node personalization-test/deploy.js
npx serve personalization-test/site

See examples/EXAMPLE.md for the full step-by-step guide.


Storage Providers

| Provider | Notes | |---|---| | s3 | AWS S3 — auto-provisions bucket + CloudFront | | r2 | Cloudflare R2 — zero egress fees | | minio | Self-hosted MinIO | | local | Local disk — dev only |

Database Providers

| Provider | Notes | |---|---| | firebase | Firebase Firestore — free tier | | supabase | Supabase Postgres | | mongodb | MongoDB Atlas |


Architecture

Developer
  └── wh ship ./components ./site
        ├── wh analyze    → detect Tailwind, GSAP, deps
        ├── wh breakdown  → extract CSS/JS from single HTML
        ├── bundle        → html + css + js → single payload
        ├── props schema  → stored in payload for runtime resolution
        ├── AES-256-GCM   → encrypt each chunk
        ├── SHA-256 hash  → integrity fingerprint
        ├── S3 upload     → store encrypted payload
        ├── HMAC sign     → project-scoped signed URL
        ├── DB register   → metadata + dep graph
        ├── wh build      → minify HTML, extract CSS/JS
        └── wh zip        → deploy.zip ready for upload

Browser
  └── <wh-component name="navbar" wh-brand="MyApp">
        ├── fetch wh-manifest.json
        ├── check token expiry
        ├── stale-while-revalidate cache
        ├── fetch from CloudFront / Edge Worker
        ├── multi-CDN failover
        ├── load CDN assets
        ├── resolve dependency graph
        ├── AES-256-GCM decrypt in memory
        ├── SHA-256 integrity verify
        ├── resolve props ({{wh.brand}} → "MyApp")
        ├── domain restriction check
        ├── inject CSS → HTML → JS (or Shadow DOM)
        ├── fire lifecycle hooks
        ├── emit metrics + plugin events
        ├── WebGPU detection
        └── Service Worker caches for offline

Latest Updates (v1.1.0)

wh analyze — Auto-generates props schema

wh analyze now scans your component HTML for {{wh.propName}} placeholders and auto-generates the props schema in webhanger.component.json:

wh analyze ./components/navbar
Props detected ({{wh.*}} placeholders):
  brand                default: ""
  link1Label           default: ""
  link1Href            default: ""
  link4Label           default: ""
  link4Href            default: ""

✅ webhanger.component.json updated with 5 props

This runs automatically on every wh deploy too — zero manual maintenance.

data-transfer-protocol on <wh-auth>

Control how auth results are sent back to your app:

<!-- redirect (default) — URL params -->
<wh-auth provider="google"
  data-transfer-protocol="redirect"
  on-success="/dashboard?name={{name}}&email={{email}}">
</wh-auth>

<!-- post — JSON body to your API -->
<wh-auth provider="github"
  data-transfer-protocol="post"
  on-success="https://api.myapp.com/auth/callback">
</wh-auth>

<!-- put — REST endpoint -->
<wh-auth provider="facebook"
  data-transfer-protocol="put"
  on-success="https://api.myapp.com/users/me/auth">
</wh-auth>

<!-- callback — JS event only, no redirect -->
<wh-auth provider="google"
  data-transfer-protocol="callback">
</wh-auth>

For post/put — your server receives:

{
  "user": { "email": "...", "name": "...", "avatar": "...", "provider": "google" },
  "token": "jwt-token"
}

With Authorization: Bearer <jwt> header. Return { "redirect": "/dashboard" } to trigger redirect.


License

ISC