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

zepp-web-runner

v0.2.0

Published

Run Zepp OS watch app pages in the browser — Vite plugin + React renderer that shims @zos/ui hmUI widgets.

Readme

Zepp Web Runner

npm version License: MIT

Run Zepp OS watch app pages in the browser — a Vite plugin + React renderer that shims @zos/ui hmUI widgets as React elements, so you can develop, debug, and iterate on your watch UI on your computer instead of pushing to a device every change.

Live Demo: antonlapshin.github.io/koala — a full Tamagotchi-style virtual pet game for Amazfit Bip 6, built with Zepp Web Runner. Try it in your browser!

Koala Game Screenshot

Why?

Developing Zepp OS apps means writing hmUI.createWidget() calls that only render on the watch. Every UI tweak requires a rebuild + deploy to device. Zepp Web Runner intercepts those widget calls and maps them to React elements, giving you instant hot-reload in the browser while your page/index.js stays unchanged — the same file runs on both platforms.

Quick Start

npm install zepp-web-runner

1. Add the Vite plugin

// vite.config.mjs
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { createZeppOsPlugin } from "zepp-web-runner/plugin";
import path from "path";

const root = path.resolve(__dirname);

export default defineConfig({
  plugins: [
    react(),
    createZeppOsPlugin({ root }),
  ],
});

2. Create the web entry point

// src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { _setPageConfig, WatchPage } from "zepp-web-runner/components/WatchPage";

// Intercept Page() calls from your watch app
globalThis.Page = _setPageConfig;

async function init() {
  // Dynamically import your watch page — this triggers Page({...})
  await import("../page/index.js");

  ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
      <WatchPage />
    </React.StrictMode>
  );
}

init();

3. Run it

npx vite

Open http://localhost:5173 — your watch app runs in the browser.

How It Works

┌──────────────────────┐
│  page/index.js        │  ← Same file as on watch
│  hmUI.createWidget()  │
│  hmUI.widget.IMG      │
│  hmUI.align.CENTER_H  │
└──────┬───────────────┘
       │  @zos/ui import
       ▼
┌──────────────────────┐
│  Vite plugin          │  ← Redirects @zos/ui → zepp-web-runner/shims/hmUI
│  createZeppOsPlugin() │
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│  hmUI shim            │  ← Captures widget calls into an array
│  _reset() / _collect()│
└──────┬───────────────┘
       │  widgets[]
       ▼
┌──────────────────────┐
│  WidgetRenderer       │  ← Maps each widget descriptor → React element
│  renderWidget(w)      │     IMG → <img>, TEXT → <div>, BUTTON → <button>, etc.
└──────┬───────────────┘
       │  React elements
       ▼
┌──────────────────────┐
│  WatchPage            │  ← React component: watch-shaped viewport
│                       │     Collects widgets, renders them cycle
└──────────────────────┘

Render cycle:

  1. page/index.js calls render() → N × hmUI.createWidget() (captured into a module-level array)
  2. WatchPage.jsx collects the array via _collect()
  3. WidgetRenderer.jsx maps each widget descriptor to a React element
  4. setState() triggers React reconciliation in the 390×450 viewport
  5. Click handlers / tick timers call render() again, repeating the cycle

Shim Mapping

| Zepp OS Import | Web Shim | Notes | |---|---|---| | import hmUI from "@zos/ui" | zepp-web-runner/shims/hmUI | Widget capture: createWidget(), enums (widget, align, event) | | import { Vibrator } from "@zos/sensor" | zepp-web-runner/shims/zos-sensor | No-op: Vibrator.start() is a no-op, Step.getCurrent() returns 0 | | import { statSync } from "@zos/fs" | zepp-web-runner/shims/zos-fs | No-op: statSync() returns false, readFileSync() returns null |

Adapter Mappings

Your watch app likely imports project-specific adapters (storage, time, sensors). Use adapterMappings to redirect them to web equivalents:

createZeppOsPlugin({
  root,
  adapterMappings: {
    // Redirect watch adapter → web adapter
    [path.resolve(root, "utils/storageAdapter.js")]:
      `export { webStorage } from "./web/adapters/storage.js";`,
    [path.resolve(root, "utils/sensorAdapter.js")]:
      `export { stepsAdapter } from "./web/adapters/steps.js";`,
  },
});

This ensures the same singleton instances are shared between page/index.js and your debug panel.

Supported Widgets

| Widget | hmUI constant | React output | Props | |---|---|---|---| | Image | widget.IMG | <img> | x, y, w, h, src + click | | Animated Image | widget.IMG_ANIM | <AnimImage> (component) | anim_path, anim_prefix, anim_ext, anim_start, anim_end, anim_fps, repeat | | Text | widget.TEXT | <div> | x, y, w, h, text, text_size, color, align_h, align_v | | Filled Rectangle | widget.FILL_RECT | <div> | x, y, w, h, color, radius + click | | Button | widget.BUTTON | <button> | x, y, w, h, text, text_size, color, normal_color, press_color, radius, click_func | | Gesture | widget.GESTURE | <GestureWidget> (component) | x, y, w, h + swipeLeft/swipeRight events |

Device Presets

Import getDevice() to look up known Zepp OS watch dimensions:

import { getDevice } from "zepp-web-runner/devices";

const dims = getDevice("balance"); // { width: 480, height: 480 }

Pass the result to WatchPage:

<WatchPage width={dims.width} height={dims.height} />

Included presets (33 devices):

| Family | Models | |---|---| | Bip | bip-6-square (390×450), bip-6-round (480×480), bip-5 (302×368), bip-5-round (416×416), bip-3-pro (240×280), bip-s-lite (176×176) | | GTR/GTS | gtr-4 (466×466), gts-4 (390×450), gtr-4-limited (466×466), gtr-3-pro (454×454), gts-3 (390×450), gtr-2 (454×454), gts-2 (348×442), gts-2-mini (306×354), gtr-mini (416×416), gts-2e (348×442), gtr-2e (454×454) | | T-Rex | t-rex-3 (480×480), t-rex-2 (454×454), t-rex-pro (360×360), t-rex-ultra (480×480) | | Falcon/Cheetah | falcon (416×416), cheetah-round (454×454), cheetah-square (390×450), cheetah-pro (454×454) | | Balance/Active | balance (480×480), active (390×450), active-edge (360×360) | | Neo | neo (160×278) | | Zepp | zepp-e-circle (416×416), zepp-e-square (390×450), zepp-z (454×454) |

API Reference

createZeppOsPlugin(opts)

Vite plugin factory. Returns a Vite plugin object.

opts: {
  root: string;                    // Project root (required for fs.allow)
  adapterMappings?: Record<string, string>;  // Absolute path → module source
  shimDir?: string;                // Override shim directory (default: built-in shims/)
}

<WatchPage width? height? />

React component that renders the watch viewport.

  • width (number, default 390) — Viewport width in pixels
  • height (number, default 450) — Viewport height in pixels

_setPageConfig(config)

Intercepts Zepp OS Page() calls. Set as globalThis.Page = _setPageConfig before importing your page module.

triggerRender()

Manually trigger a re-render (e.g. from a debug panel after changing state).

renderWidget(widget)

Maps a single widget descriptor to a React element. Useful for custom widget handling.

getDevice(preset)

Look up a device by preset name. Returns { width, height } or fallback { width: 390, height: 450 } for unknown presets.

Image Assets

The renderer expects images to be served at ${BASE_URL}images/. Place your watch assets in public/images/ in your Vite project:

public/
  images/
    koala/
      koala_1.png
      koala_2.png
      ...
    ui/
      food.png
      toy.png
      ...
    bg_day.png
    bg_night.png

The src prop in hmUI.createWidget(hmUI.widget.IMG, { src: "koala/koala_1.png" }) maps to public/images/koala/koala_1.png.

Known Limitations

  • Vibrator is a no-op in the browser — no haptic feedback
  • Image animation (IMG_ANIM) uses setInterval; frame timing may drift from the watch's native implementation
  • File system (@zos/fs) shims return no data — implement your own storage adapter for the web
  • Sensors (@zos/sensor) return zero values — implement your own sensor adapter for the web
  • Multi-page apps need one WatchPage per page; the shim currently supports a single active page config
  • Colors are interpreted as ARGB32 integers (0xAARRGGBB)

License

MIT — see LICENSE.