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

@mwguerra/hull

v0.1.3

Published

Tiny native desktop apps from your Vue/React UI — a prebuilt C++ web-view host you drive with npm scripts. No compiler, no Electron.

Readme

@mwguerra/hull

Tiny native desktop apps from your Vanilla-JS / React / Vue UI — a prebuilt C++ web-view host you drive with npm scripts. No compiler, no Electron, no bundled browser engine.

Hull ships a small prebuilt native binary that renders your existing Vite app in the operating system's web view (WebView2 / WebKit / WebKitGTK) and exposes a JSON bridge to a C++ backend with batteries included: TLS HTTP, encrypted storage, OS keychain, SQLite, files, and printing. Your app stays plain JS/React/Vue.

This README is the full reference for now.

Contents

Quick start

In any existing Vite app (Vanilla JS, React, or Vue):

npm i -D @mwguerra/hull

Add scripts to package.json:

{
  "scripts": {
    "dev": "hull dev",      // Vite dev server in a native window (HMR)
    "build": "hull build",  // single-file UI, packaged with the host -> ./release
    "start": "hull start"   // run the packaged build
  }
}

npm run dev opens your app as a desktop window. Zero config — the window title and a per-app storage namespace are derived from package.json, and the window ships with the Hull logo as its icon until you set your own. Installing @mwguerra/hull also pulls the prebuilt host for your OS/CPU automatically (an os/cpu-gated optional dependency, e.g. @mwguerra/hull-win32-x64).

Starting a brand-new project? Copy one of the recipes below.

Integrate your project

The C++ backend and the JSON bridge (@mwguerra/hull/bridge) are identical across frameworks — only the UI layer and the optional state hook differ. Each recipe below is the exact shape of a runnable example in the repo (examples/vanilla-js, examples/react, examples/vue); every example exercises all features (bridge, settings + C++→UI events, credentials, HTTP, printing, SQLite, files, single-image upload). Copy the one you want and trim.

Every Hull project, regardless of framework, has:

  • the @mwguerra/hull dev dependency + the npm scripts,
  • a normal vite.config.js (Hull injects the single-file plugin only at build time),
  • an index.html Vite entry,
  • an optional .hullrc,
  • your UI code, which imports from @mwguerra/hull/bridge.

Project layout (same for all three):

my-app/
├── package.json
├── vite.config.js
├── .hullrc            # optional
├── index.html
└── src/
    ├── main.js|.jsx   # Vite entry
    ├── App.vue|.jsx   # (React/Vue) your root component
    └── style.css

Vanilla JS

package.json:

{
  "name": "my-app",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "hull dev",
    "dev:browser": "hull dev --browser",
    "build": "hull build",
    "start": "hull start",
    "web": "vite"
  },
  "devDependencies": {
    "@mwguerra/hull": "^0.1.0",
    "vite": "^6.0.0"
  }
}

vite.config.js:

import { defineConfig } from "vite";
// Plain Vite — no framework plugin. `hull build` adds the single-file plugin.
export default defineConfig({});

index.html:

<!doctype html>
<html lang="en">
  <head><meta charset="UTF-8" /><title>My App</title></head>
  <body>
    <button id="ping">Send to C++</button>
    <pre id="out"></pre>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

src/main.js — the bridge with no framework (use nativeSetting for two-way state):

import { ping, db, nativeSetting, hasBridge } from "@mwguerra/hull/bridge";

// 1) call C++ and show the result
document.querySelector("#ping").addEventListener("click", async () => {
  const res = await ping("hello");                 // -> { ok: true, echo: "hello" }
  document.querySelector("#out").textContent = JSON.stringify(res);
});

// 2) a two-way persisted setting (C++ stores it; C++ pushes changes back)
const theme = nativeSetting("theme");
theme.subscribe((v) => document.documentElement.classList.toggle("dark", v === "dark"));
theme.load();                                      // initial pull (no-op in a plain browser)
// theme.set("dark") persists and notifies subscribers

// 3) SQLite — works in the native host or browser dev mode
if (hasBridge()) {
  db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"])
    .then(() => db.query("SELECT * FROM notes ORDER BY id DESC"))
    .then((notes) => console.log(notes))
    .catch(console.error);
}

React

package.json:

{
  "name": "my-app",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "hull dev",
    "dev:browser": "hull dev --browser",
    "build": "hull build",
    "start": "hull start",
    "web": "vite"
  },
  "dependencies": { "react": "^18.3.0", "react-dom": "^18.3.0" },
  "devDependencies": {
    "@mwguerra/hull": "^0.1.0",
    "@vitejs/plugin-react": "^4.3.0",
    "vite": "^6.0.0"
  }
}

vite.config.js:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({ plugins: [react()] });

index.html:

<!doctype html>
<html lang="en">
  <head><meta charset="UTF-8" /><title>My App</title></head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

src/main.jsx:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./style.css";

createRoot(document.getElementById("root")).render(
  <StrictMode><App /></StrictMode>
);

src/App.jsx — bridge + the useNativeState hook:

import { useEffect, useState } from "react";
import { ping, db, hasBridge } from "@mwguerra/hull/bridge";
import { useNativeState } from "@mwguerra/hull/react";

export default function App() {
  const [out, setOut] = useState(null);
  const [theme, setTheme] = useNativeState("theme");   // like useState, persisted in C++
  const [notes, setNotes] = useState([]);

  useEffect(() => {
    document.documentElement.classList.toggle("dark", theme === "dark");
  }, [theme]);

  useEffect(() => {
    if (!hasBridge()) return;                           // native host or browser dev mode
    (async () => {
      await db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"]);
      setNotes(await db.query("SELECT * FROM notes ORDER BY id DESC"));
    })();
  }, []);

  return (
    <>
      <button onClick={async () => setOut(await ping("hello"))}>Send to C++</button>
      {out && <pre>{JSON.stringify(out)}</pre>}
      <select value={theme ?? ""} onChange={(e) => setTheme(e.target.value)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
      <ul>{notes.map((n) => <li key={n.id}>{n.body}</li>)}</ul>
    </>
  );
}

Vue

package.json:

{
  "name": "my-app",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "hull dev",
    "dev:browser": "hull dev --browser",
    "build": "hull build",
    "start": "hull start",
    "web": "vite"
  },
  "dependencies": { "vue": "^3.5.0" },
  "devDependencies": {
    "@mwguerra/hull": "^0.1.0",
    "@vitejs/plugin-vue": "^5.2.0",
    "vite": "^6.0.0"
  }
}

vite.config.js:

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({ plugins: [vue()] });

index.html:

<!doctype html>
<html lang="en">
  <head><meta charset="UTF-8" /><title>My App</title></head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

src/main.js:

import { createApp } from "vue";
import App from "./App.vue";
import "./style.css";

createApp(App).mount("#app");

src/App.vue — bridge + the useNativeState hook:

<script setup>
import { ref, watch, onMounted } from "vue";
import { ping, db, hasBridge } from "@mwguerra/hull/bridge";
import { useNativeState } from "@mwguerra/hull/vue";

const out = ref(null);
async function send() { out.value = await ping("hello"); }

const theme = useNativeState("theme");   // a ref; edits persist in C++, C++ pushes back
watch(theme, (v) => document.documentElement.classList.toggle("dark", v === "dark"),
  { immediate: true });

const notes = ref([]);
onMounted(async () => {
  if (!hasBridge()) return;              // native host or browser dev mode
  await db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"]);
  notes.value = await db.query("SELECT * FROM notes ORDER BY id DESC");
});
</script>

<template>
  <button @click="send">Send to C++</button>
  <pre v-if="out">{{ out }}</pre>
  <select v-model="theme">
    <option value="light">Light</option>
    <option value="dark">Dark</option>
  </select>
  <ul><li v-for="n in notes" :key="n.id">{{ n.body }}</li></ul>
</template>

Shared .hullrc + run (all three)

.hullrc (optional — see Configuration):

{
  "appId": "com.you.my-app",
  "window": { "title": "My App", "width": 1100, "height": 760 }
}

Then:

npm install
npm run dev           # native window with HMR (+ a dev inspector tab)
npm run dev:browser   # run the UI in your browser with the full bridge, no recompile
npm run build         # single-file the UI + package with the host -> ./release
npm run start         # run the packaged app

The three recipes differ only in the UI layer. @mwguerra/hull/bridge (ping, db, files, settings, credentials, http, printers) is the same in all of them; @mwguerra/hull/vue and @mwguerra/hull/react add the useNativeState hook (Vanilla JS uses nativeSetting directly). For the complete, feature-by-feature versions, see the examples/ apps in the repo.

Try it from a blank project

Scaffold a fresh Vite app, add Hull, package it, and open the desktop window. The same commands work on Windows, macOS, and Linux@mwguerra/hull pulls the prebuilt host for your OS/CPU automatically, and hull start opens the packaged app.

Vue:

npm create vite@latest my-hull-app -- --template vue
cd my-hull-app
npm install
npm i -D @mwguerra/hull
npx hull build      # bundle the UI + package it with the native host
npx hull start      # open the desktop app

React:

npm create vite@latest my-hull-app -- --template react
cd my-hull-app
npm install
npm i -D @mwguerra/hull
npx hull build
npx hull start

Want a live-reload window without packaging first? Run npx hull dev instead of build + start. Plain JS works too — use --template vanilla. To wire hull into your npm scripts, see the recipes above.

Talking to the backend

Every call goes UI → C++ and returns a Promise; all the real work happens in the native host.

import { ping, httpPost, saveCredential, isNative } from "@mwguerra/hull/bridge";

await ping("hello");                                  // -> { ok: true, echo: "hello" }
const res = await httpPost("https://api.example.com/x", { a: 1 }); // TLS, in C++
await saveCredential("api.example.com", "default", token);        // -> OS keychain

Structured persistence with embedded SQLite (parameterized, stored per-user):

import { db } from "@mwguerra/hull/bridge";
await db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"]);
await db.exec("INSERT INTO notes (body) VALUES (?)", ["hello"]);
const notes = await db.query("SELECT * FROM notes ORDER BY id DESC");

Files / uploads (e.g. show an uploaded image — the pattern the examples use):

import { files } from "@mwguerra/hull/bridge";
await files.write(file.name, file);                  // string | Uint8Array | ArrayBuffer | Blob
const bytes = await files.read(file.name);           // Uint8Array
const url = URL.createObjectURL(new Blob([bytes], { type: "image/png" }));
imgEl.src = url;                                      // preview; URL.revokeObjectURL(url) later

Two-way persisted state (plaintext by default; encrypted at rest in the secure build):

// Vue
import { useNativeState } from "@mwguerra/hull/vue";
const theme = useNativeState("theme");   // a ref; edits persist, C++ pushes sync back

// React
import { useNativeState } from "@mwguerra/hull/react";
const [theme, setTheme] = useNativeState("theme");

// Vanilla JS
import { nativeSetting } from "@mwguerra/hull/bridge";
const theme = nativeSetting("theme");    // .get() / .set(v) / .subscribe(fn) / .load()

Bridge API reference

All from @mwguerra/hull/bridge:

| Function | Backend | |----------|---------| | ping(text) | sync echo (diagnostics) | | httpPost(url, body) / httpGet(url) | cpp-httplib + OpenSSL, on a worker thread; injects a Bearer token from the keychain | | saveSetting / loadSetting / loadAllSettings | per-user store (plaintext by default; AES in the secure build) | | nativeSetting(key) | two-way setting store: .get() / .set(v) / .subscribe(fn) / .load() | | saveCredential / credentialExists / eraseCredential | OS keychain; write-only — secrets never return to JS | | listPrinters | discover printers (Winspool / CUPS) | | printMessage(printer, text) | print a text document — works with any printer (Print to PDF, OneNote, laser) | | printReceipt(printer, text) / printNetwork(host, port, text) | raw ESC/POS for thermal receipt printers (spooler / TCP port-9100) | | db.query / db.get / db.exec / db.batch / db.migrate | embedded SQLite, parameterized, per-user storage | | files.write / read / readText / list / remove | file/upload storage in the per-user dir (through the secure layer) | | appInfo() | { ok, appId, secure }secure true on a crypto build | | bridge.on(event, fn) | subscribe to C++ → UI push events (e.g. settings:changed); returns an unsubscribe fn | | hasBridge() / isNative() / bridgeMode() | hasBridge = reachable (native or browser dev); isNative = native web view; bridgeMode = "native"/"http"/"none" |

Framework hooks: useNativeState(key) from @mwguerra/hull/vue (returns a ref) and @mwguerra/hull/react (returns [value, setValue]).

CLI commands

| Command | What it does | |---------|--------------| | hull dev | Vite dev server rendered in a native window (HMR) + a dev inspector tab | | hull dev --browser | run the UI in your browser with the full bridge over HTTP/SSE (no recompile) | | hull build [vX.Y.Z] | single-file the UI and package it with the host into release/<version\|development>/<platform>/ + an archive | | hull build … --platform <key\|all> | also package other platforms whose host binary is present; --format zip\|tar.gz | | hull start [vX.Y.Z] | run a packaged build | | hull installer [vX.Y.Z] | wrap the build into a native installer — .dmg (macOS), .deb (Linux), .exe (Windows) | | hull doctor | check this machine for everything Hull needs (host binary, web view, system libraries) with copy-pasteable fixes | | hull eject | copy the C++ host project into ./desktop to add native bindings |

Add -v / --verbose to any command for per-step timings (every command prints its total time). Add --debug to dev/start for a verbose host log on stderr plus the web-view devtools — the first tool to reach for if a window opens blank. The version argument must match vX.Y.Z (optionally -suffix); with no version, output goes to a development/ folder.

Linux dev machines need the host's runtime libraries (WebKitGTK, libsecret, CUPS): sudo apt install libwebkitgtk-6.0-4 libsecret-1-0 libcups2. hull start/ hull dev check this up front and print the exact command for your distro; the .deb from hull installer declares them automatically for end users.

When passing flags like --platform through npm run, the -- separator is required: npm run build -- --platform all forwards the flag, but without -- npm silently drops it. npx hull … avoids the footgun entirely.

Configuration (.hullrc)

Drop a .hullrc (JSON) in your project root — only the keys you set override the package defaults. Lookup order: .hullrc.hullrc.jsonhull.config.json.

{
  "appId": "com.you.notes",
  "secure": false,
  "window": { "title": "Notes", "width": 1200, "height": 800, "icon": "build/icon.png" }
}

| Key | Default | Meaning | |-----|---------|---------| | appId | com.hull.<pkg name> | namespaces the store, DB, files, and keychain entries so multiple Hull apps never collide | | window.title | pkg productName/name | native window title | | window.width / window.height | 1100 / 760 | window size | | window.icon (or top-level icon) | bundled Hull logo | PNG/ICO for the window/app icon; set at runtime on Windows (GDI+), via the app bundle on macOS/Linux; SVG is not a valid native icon | | secure | false | run the crypto host build (hull-host-secure): AES files/settings + SQLCipher DB | | debug | false | open the web-view dev tools | | outDir | dist | Vite UI build dir | | releaseDir | release | packaged-app output dir |

Develop in the browser (no recompile)

npm run dev -- --browser    # or: npx hull dev --browser

Runs the UI in your browser with full Vite HMR while bridge calls still reach the real native backend over HTTP/SSE — change a label, hit reload, no recompile. Both hull dev and --browser also open a dev-only inspector (live bridge calls, events, DB/file ops, timings) that is stripped from production builds (import.meta.env.DEV dead-code elimination).

Versioned releases

npm run build              # -> release/development/<platform>/ + archive
npm run build -- v1.2.3    # -> release/v1.2.3/<platform>/ + archive

Each build emits a self-contained, versioned bundle and a ready-to-ship archive (.zip on Windows, .tar.gz on macOS/Linux) with the minimal runnable set — the host binary, the libraries it needs, your inlined app.html, a double-click launcher, and icon.png if you configured one. Unpack on the target and run. --platform all also packages other platforms whose host binary is installed (realistically produced via CI, one runner per OS). With secure: true, bundle dirs and archives get a -secure suffix.

Native installers

After a build, wrap it into a native installer for the current OS:

npm run build && npx hull installer       # -> release/<version>/<App>-<version>-<key>.<dmg|deb|exe>

| OS | Output | Tooling | |----|--------|---------| | macOS | .dmg (the .app + an Applications drop-link) | hdiutil (built in) | | Linux | .deb (installs to /opt, registers the .desktop + icon, deps via dpkg-shlibdeps) | dpkg-deb (built in) | | Windows | .exe (per-user install, Start-Menu/Desktop shortcuts, uninstaller) | Inno Setupwinget install JRSoftware.InnoSetup |

Each is built on its own OS (the tools are OS-native), like the host. Install with: double-click the .dmg and drag to Applications; sudo apt install ./<app>.deb; run the .exe. Unsigned for now — for distribution to other machines, add code-signing (macOS notarization / Windows Authenticode) as a later step.

Security (at-rest crypto is a build option)

Default build = no crypto, everything fast (plaintext at rest; secrets still in the keychain). For encryption at rest, use the secure build:

npm run build:host:secure    # AES for files/settings + SQLCipher for the DB
# then in .hullrc: { "secure": true }

Files and the DB go through one crypto layer — nothing calls cryptography directly. SQLite is also hardened in all builds: PRAGMA trusted_schema=OFF on every connection, and the default build compiles with SQLITE_OMIT_LOAD_EXTENSION and SQLITE_DQS=0. Queries are always parameterized (bound in C++), and exec/query/get run one statement each.

Custom native code

Need your own C++ binding? Run hull eject to copy the host project into ./desktop, add a binding (d.on("myThing", (args, reply) => reply({ ok: true }))), and build it with CMake. The standard bindings (HTTP / storage / keychain / printing / DB / files) are already there to extend. See desktop/README.md.

Platform support

| | Windows | macOS | Linux | |---|---------|-------|-------| | Web view | WebView2 (Edge) | WebKit | WebKitGTK 6 | | Credentials | Credential Manager | Keychain | libsecret | | Printing | Winspool | CUPS | CUPS | | Window icon | runtime (GDI+) | .app bundle (built by hull build) | auto .desktop + icon-theme install | | hull build output | folder + .cmd launcher (zip) | .app bundle (tar.gz) | folder + .sh launcher (tar.gz) | | Build the host on | Windows | macOS | any OS via Docker, or native Linux |

End users only need the OS web-view runtime (preinstalled on Windows 11 and macOS; libwebkitgtk-6.0 on Linux). A host must be built on its own OS — true cross-compile isn't realistic for WebView2/WebKit — except Linux, which builds from any OS via Docker.

Linux sandbox note: WebKitGTK sandboxes its subprocesses with bubblewrap, which needs unprivileged user namespaces. They're blocked on Ubuntu 24.04 (AppArmor default) and in many containers, which otherwise crashes the app with bwrap: setting up uid map: Permission denied. Hull's host auto-detects this and disables the sandbox so the app still runs (with a notice). Override with hull start --no-sandbox, .hullrc { "linux": { "sandbox": false } }, or keep it by enabling userns (sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0). See platforms.md.

Linux icon note: GTK4 has no runtime "set icon from a PNG", so on hull dev/start the host auto-installs desktop integration — it writes ~/.local/share/applications/<appId>.desktop and the icon into the user icon theme, and sets the window's app-id so the compositor shows it (Wayland + X11). A new icon may need a moment or a re-login for the shell to pick it up. See configuration.md.

How it works

  • Prebuilt host binaries are delivered as platform-gated optional dependencies (@mwguerra/hull-win32-x64, …) — npm installs only the one for your machine.
  • hull build uses your project's Vite plus vite-plugin-singlefile to inline the whole UI into one HTML file, then bundles it with the host.
  • The host loads that file at runtime (--app) in production, or your dev server (--url) during development. The bridge is exposed over the web view natively, or over HTTP/SSE in browser dev mode.

License

MIT