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

electron-updater-for-render

v2.0.1

Published

A lightweight incremental updater for Electron renderer processes

Downloads

131

Readme

electron-updater-for-render

中文文档

A highly robust, hybrid (auto-prompt & API-driven) incremental OTA updater for Electron renderer processes (Vue / React / plain HTML).

Unlike electron-updater which re-downloads the entire app installer, this library updates only the renderer assets (HTML/CSS/JS) using .asar files — no elevated permissions, no installer popups, just a fast and silent hot-patch.


🚀 Features

  • Dual-Track Update Mode: Built-in native OS dialogs or a fully custom frontend progress UI via IPC — your choice
  • Three-State Check System: check() distinctly returns idle / available / ready, allowing the UI to accurately distinguish between "No Updates", "Download Required", and "Ready to Restart" for a seamless state transition flow
  • Ready-State Persistence: Intelligently detects locally cached complete downloads. Even if the user delays installation or refreshes the page, the update state remains "Ready", ensuring an uninterrupted user journey
  • Real-time Status Push: The onStatusChanged hook enables the main process to proactively notify the renderer layer to refresh the UI when updates are locally ready
  • Force Update: Push forceUpdate: 'prompt' or 'silent' from your server for P0 hotfixes that bypass the "Later" button entirely
  • Graceful Restart Hook: onBeforeRestart async hook lets the renderer save unsaved state before app.relaunch()
  • Concurrency Lock: Prevents race conditions between auto-check-on-boot and manual-trigger-on-click
  • Smart Cache Pruning: maxVersionsToKeep automatically removes outdated ASAR bundles while retaining rollback copies
  • Stream Pipeline Download: Node.js stream/promises pipeline + Transform — zero memory leaks, full backpressure support
  • Zero-config Version Detection: Auto-reads version from process.cwd()/package.json — no extra config required
  • Separate Build Commands: Normal npm run build is untouched. Only npm run build:update triggers ASAR packaging
  • History Mode Support: Built-in support for Vue/React History router mode.
  • Multi-Page Application (MPA): Support for loading multiple entry points via getLoadUrl(entry).
  • Simplified SDK: Unified exports for /main, /preload, and /renderer, including getRouterBase.
  • Precision Canary Releases: Use identity.deviceId and rolloutRule to drop updates silently to a whitelist of device IDs.
  • Semver Beta Channels: Built-in Beta/RC channel toggling via allowPrerelease guarantees Stable users are isolated from beta patches.
  • Dynamic Request Gateways: Send custom Authenticated Headers (e.g., Oauth/Bearer tokens) and Queries via requestOptions when checking for updates.

📦 Installation

npm install electron-updater-for-render

# asar is required for the build step (in your renderer project)
npm install -D @electron/asar

🗺️ How It Works

┌─────────────────────────────────────────────────────┐
│  npm run build:update                               │
│  → native build → CLI packs ASAR → latest.json     │
└───────────────┬─────────────────────────────────────┘
                │  Upload dist_updates/ to server
                ▼
┌─────────────────────────────────────────────────────┐
│  Update Server (Nginx / S3 / CDN)                   │
│  serves: latest.json + /1.0.2/renderer.asar         │
└───────────────┬─────────────────────────────────────┘
                │  HTTP fetch on app startup
                ▼
┌─────────────────────────────────────────────────────┐
│  Electron Main Process                              │
│  updater.check() → 'idle' | 'available' | 'ready'  │
│  → download ASAR → setUpdatePending()               │
│  → onStatusChanged push → renderer UI updates       │
│  → on restart: applyUpdate → app.relaunch()         │
└─────────────────────────────────────────────────────┘

Three-State Lifecycle

| State | Meaning | UI Action | |---|---|---| | idle | No update found | Show "Check for Updates" | | available | New version found, not downloaded | Show "Download Update" | | ready | Update downloaded, pending restart | Show "Restart to Install" |

The ready state is persisted to disk (current.json). Even after a page refresh or app re-open, the UI correctly shows "Restart to Install" without asking the user to re-download.


🛠️ Complete Setup Guide

Step 1 — Renderer Project: Define Config

In the root of your frontend project (Vite, Webpack, etc.), create updater.config.ts:

import { defineConfig } from 'electron-updater-for-render/builder'

export default defineConfig({
  outDir: './dist',              // Required: Your build output directory
  updatesDir: './dist_updates',  // Optional: where to write update packages (default: './dist_updates')
  // rolloutRule: {
  //   deviceIds: ['YOUR_DEVICE_ID'] // Optional: Whitelist of device IDs for Canary/Staged rollouts
  // },
  // version: '1.2.3'           // Optional: explicit version (overrides package.json)
  // packageJsonPath: './package.json'  // Optional: custom package.json path
  // forceUpdate: 'prompt'      // Optional: 'prompt' | 'silent' — for P0 mandatory rollouts ONLY
})

Version resolution priority:

  1. version field (explicit)
  2. packageJsonPath → read version from that file
  3. Auto-detect: process.cwd()/package.json (default — zero config)

Step 2 — Renderer Project: Add Build Script

In your package.json, chain your build command with the updater CLI:

{
  "scripts": {
    "build": "your native build command (vite build or webpack)",
    "build:update": "npm run build && electron-updater-for-render"
  }
}

npm run build — normal frontend build, no ASAR packing npm run build:update — builds frontend + CLI packs ASAR + writes latest.json

💡 Advanced Usage: The CLI looks for updater.config.ts by default. If you use a custom path, pass it via -c or --config: "build:update": "npm run build && electron-updater-for-render -c custom/path.config.ts"

Step 3 — Deploy Update Files

After running npm run build:update, the dist_updates/ directory will contain:

dist_updates/
├── latest.json          ← version manifest (fetched by the client)
└── 1.0.2/
    └── renderer.asar    ← the actual update payload

Upload everything in dist_updates/ to your update server (Nginx, S3, CDN, GitHub Releases, etc.).

Example upload:

# Example: sync to S3 bucket
aws s3 sync dist_updates/ s3://your-bucket/auto-updates/

# Example: upload to Nginx server
rsync -avz dist_updates/ user@your-server:/var/www/auto-updates/

The server must serve these files over HTTP(S). The client will fetch:

  • GET https://your-server.com/latest.json
  • GET https://your-server.com/1.0.2/renderer.asar

Step 4 — Electron Main Process: Configure Runtime

// src/main/index.ts
import { app, BrowserWindow } from 'electron'
import { join } from 'path' 
import { RenderUpdater, setupUpdaterIPC } from 'electron-updater-for-render/main'

const updater = new RenderUpdater({
  updateUrl: 'https://your-server.com/auto-updates', // Required: base URL for updates
  versionsDir: join(app.getPath('userData'), 'versions'), // Required: local version storage
  
  // Cloud Rollout Rules Support
  identity: { deviceId: 'USER_LOCAL_UUID' },
  // Beta Channel Support
  allowPrerelease: true,
  // Custom Authenticated Gateway
  requestOptions: {
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  },

  maxVersionsToKeep: 2, // Optional: keep 2 old versions for rollback (default: 2)

  // Optional: Delay the restart by 2 seconds to give the Vue renderer time to save state
  onBeforeRestart: async () => {
    await new Promise(resolve => setTimeout(resolve, 2000))
  }
})

// 🎉 Automatically sets up the entire IPC bus in one line
// It exposes API handles and automatically broadcasts progress, status, and restart events to all webContents
setupUpdaterIPC(updater)

app.whenReady().then(async () => {
  const mainWindow = new BrowserWindow({
    webPreferences: { preload: join(__dirname, '../preload/index.js') }
  })

  // Load the latest downloaded ASAR, fallback to local dist if no update is available
  // Supports multi-page entries (default: 'index.html')
  const loadUrl = updater.getLoadUrl('login/index.html')
  mainWindow.loadURL(loadUrl || `file://${join(__dirname, '../renderer/login/index.html')}`)

  // Auto-check on startup (respects forceUpdate, shows dialogs if autoPrompt: true)
  setTimeout(() => updater.checkForUpdatesAndNotify(), 3000)
})

Step 5 — Preload Script

// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
import { exposeUpdaterPreload } from 'electron-updater-for-render/preload'

// Expose the API under the 'updaterAPI' namespace, as expected by getUpdater()
contextBridge.exposeInMainWorld('updaterAPI', exposeUpdaterPreload(ipcRenderer))

Step 6 — Renderer UI (Vue Example)

The following example demonstrates the complete update flow with:

  • Auto-check on mount — no manual click required to detect updates
  • Three-state awareness — correctly shows "Download" vs "Restart" based on check() result
  • Real-time status push — UI updates instantly when background downloads complete
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { getUpdater, getRouterBase } from 'electron-updater-for-render/renderer'

// Automatically detect the correct context-aware History base for MPA
const routerBase = getRouterBase() 
const updater = getUpdater()

// Status: 'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'
const status = ref<'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'>('idle')
const progress = ref(0)
const newVersion = ref('')
const updateInfo = ref<any>(null)  // Store the full UpdateInfo for passing to download()
const errMsg = ref('')

let removeProgress: (() => void) | null = null
let removeRestart: (() => void) | null = null
let removeStatusChanged: (() => void) | null = null

const updater = getUpdater()

// ── Check for updates ──────────────────────────────────────────────────────
// check() returns { status: 'idle' | 'available' | 'ready', version, info }
// 'ready' means an update is already downloaded and waiting for restart.
const checkUpdate = async () => {
  status.value = 'checking'
  try {
    const res = await updater.check()
    if (res.updateAvailable) {
      newVersion.value = res.version
      updateInfo.value = res.info
      // Directly jump to 'ready' if the update is already downloaded
      status.value = res.status === 'ready' ? 'ready' : 'available'
    } else {
      status.value = 'no-update'
    }
  } catch (err: any) {
    errMsg.value = 'Check failed: ' + err.message
    status.value = 'idle'
  }
}

// ── Download update ────────────────────────────────────────────────────────
// Pass the UpdateInfo object from check() so the main process doesn't need
// to re-fetch latest.json, preventing version mismatches.
const downloadUpdate = async () => {
  status.value = 'downloading'
  progress.value = 0

  removeProgress = updater.onDownloadProgress((percent: number) => {
    progress.value = percent
  })

  try {
    await updater.download(updateInfo.value)
    status.value = 'ready'
  } catch (err: any) {
    errMsg.value = 'Download failed: ' + err.message
    status.value = 'available'
  } finally {
    removeProgress?.()
  }
}

// ── Install & restart ──────────────────────────────────────────────────────
const installUpdate = () => {
  updater.installAndRestart()
}

// ── Real-time status push from main process ───────────────────────────────
// When a download completes in the background, the main process fires
// onStatusChanged. We update the UI immediately without any user action.
const initStatusListener = () => {
  if (updater?.onStatusChanged) {
    return updater.onStatusChanged((data: { status: string; version: string }) => {
      if (data.status === 'ready') {
        status.value = 'ready'
        newVersion.value = data.version
      }
    })
  }
  return null
}

// ── Graceful restart ───────────────────────────────────────────────────────
const initRestartListener = () => {
  if (updater?.onBeforeRestart) {
    return updater.onBeforeRestart(() => {
      // Save any unsaved state here before the app relaunches
      console.log('App is about to restart, saving state...')
    })
  }
  return null
}

// ── Lifecycle ──────────────────────────────────────────────────────────────
onMounted(() => {
  removeStatusChanged = initStatusListener()
  removeRestart = initRestartListener()

  // Auto-check 2 seconds after mount (non-blocking for initial render)
  setTimeout(checkUpdate, 2000)
})

onUnmounted(() => {
  removeProgress?.()
  removeRestart?.()
  removeStatusChanged?.()
})
</script>

<template>
  <div class="updater">
    <div v-if="errMsg" class="error">{{ errMsg }}</div>

    <!-- No active update check -->
    <button v-if="status === 'idle' || status === 'no-update'" @click="checkUpdate">
      {{ status === 'no-update' ? 'Up to date — Check Again' : 'Check for Updates' }}
    </button>

    <!-- Checking -->
    <span v-if="status === 'checking'">Checking for updates...</span>

    <!-- Update available, not yet downloaded -->
    <div v-if="status === 'available'">
      <p>🔥 New version available: v{{ newVersion }}</p>
      <button @click="downloadUpdate">Download Update</button>
    </div>

    <!-- Downloading -->
    <div v-if="status === 'downloading'">
      <p>Downloading... {{ progress.toFixed(1) }}%</p>
      <progress :value="progress" max="100" />
    </div>

    <!-- Update ready to install (persisted across page refreshes) -->
    <div v-if="status === 'ready'">
      <p>✅ v{{ newVersion }} is ready to install</p>
      <button @click="installUpdate">Restart & Install Now</button>
    </div>
  </div>
</template>

🛣️ Routing Modes (Hash vs History)

The library supports both standard Electron file-loading (Hash) and modern SPA routing (History) via custom protocols.

1. Hash Mode (Recommended: Zero-Config, Zero-Intrusion)

The industry standard for Electron applications. It is resilient to physical path changes, requires no base configuration, and needs zero plugin-related JS in your frontend.

  • Main Process: No extra config needed (defaults to 'hash').
  • Renderer (Vite): Set base: './' (or omit) in vite.config.ts.
  • Renderer (Router): Use createWebHashHistory().
  • Pros: Pure decoupling; your frontend project remains completely unaware of the updater's existence.

2. History Mode (Advanced)

Supports clean URLs and native browser refresh behavior. Due to physical path context in MPAs, the router needs to be aligned with the base path.

  • Main Process:

    new RenderUpdater({
      // ...
      routerMode: 'history',
      protocol: 'my-app'  // Optional: custom protocol name (default: 'app')
    })
  • Renderer (Router) - Option A: Use Automated Helper (Recommended) Use our lightweight helper to automatically sense the physical base of the current window:

    import { getRouterBase } from 'electron-updater-for-render/renderer'
    const router = createRouter({
      history: createWebHistory(getRouterBase()), // Auto-aligns with the app:// protocol path
      routes: [...]
    })
  • Renderer (Router) - Option B: Manual Alignment (Zero-Intrusion) If you prefer not to import any plugin JS, you can manually provide a string that matches the path passed to getLoadUrl() (must end with .html/):

    const router = createRouter({
      history: createWebHistory('/index.html/'), 
      routes: [...]
    })

3. Multi-Page Application (MPA)

  • Multi-window Support: Load different entry points for different windows.
  • History Refresh Support: Supports browser refresh in History mode.
  • Base Path Detection: Use getRouterBase() to automatically identify the current routing base.

⚙️ API Reference

BuilderOptions (CLI Config)

| Option | Type | Required | Description | |---|---|---|---| | outDir | string | ✅ | Vite/Webpack build output directory | | updatesDir | string | — | Output directory for update packages. Default: ./dist_updates | | rolloutRule | object | — | Target whitelists: { deviceIds: string[] }. | | version | string | — | Explicit version string, overrides packageJsonPath | | packageJsonPath | string | — | Custom path to package.json. Defaults to process.cwd()/package.json | | asarName | string | — | ASAR filename. Default: renderer.asar | | privateKeyPath | string | — | Path to RSA private key for signing | | releaseNotesPath | string | — | Path to release notes markdown file | | forceUpdate | 'prompt' \| 'silent' | — | Mandatory update mode. 'prompt' = blocking dialog; 'silent' = fully invisible |

UpdaterOptions (Main Process)

| Option | Type | Required | Description | |---|---|---|---| | updateUrl | string | ✅ | Base URL where update files are hosted | | versionsDir | string | ✅ | Local directory to store downloaded ASAR versions | | identity | object | — | Fingerprint for rollouts logic (must include deviceId). | | allowPrerelease | boolean | — | Skips prerelease tags if false. | | requestOptions | object | — | Custom HTTP headers and query params. | | publicKey | string | — | RSA public key (PEM) for signature verification | | autoDownload | boolean | — | Auto-download without asking. Default: false | | autoPrompt | boolean | — | Show built-in native dialogs. Default: true | | maxVersionsToKeep | number | — | Number of old versions to retain for rollback. Default: 2 | | routerMode | 'hash' \| 'history' | — | Routing mode. Default: 'hash' | | protocol | string | — | Custom protocol name for history mode. Default: 'app' | | onUpdateAvailable | function | — | Custom hook: (info, doDownload) => void | | onDownloadProgress | function | — | Progress callback: (percent: number) => void | | onDownloadComplete | function | — | Completion hook: (info, doInstall) => void | | onStatusChanged | function | — | Status push hook: (data: { status: 'ready' \| 'available' \| 'idle', version: string }) => void | | onError | function | — | Error handler: (error: Error) => void | | onBeforeRestart | async function | — | Called before app.relaunch(). Await-able for graceful shutdown |

RenderUpdater Instance Methods

| Method | Returns | Description | |---|---|---| | check() | Promise<CheckResult> | Check for updates. Returns { updateAvailable, status, version, info }. Status is 'idle' \| 'available' \| 'ready' | | download(info?, onProgress?) | Promise<void> | Download the update. Pass the info object from check() to avoid redundant network requests | | getLoadUrl(entry?) | string | Returns the URL to load the latest ASAR (app://renderer/ or file://...). entry defaults to index.html. Returns empty string if no update downloaded | | installAndRestart() | Promise<void> | Apply the pending update and relaunch the app | | checkForUpdatesAndNotify() | Promise<void> | All-in-one: check + prompt + download + install with built-in native dialogs | | setUpdatePending(version) | void | Mark a version as downloaded and pending restart. Also fires onStatusChanged | | useVersion(version) | void | Immediately switch both disk and memory version pointers | | activeVersion | string (getter) | The version currently loaded in memory (session) | | pendingVersion | string (getter) | The latest downloaded version on disk | | isUpdatePending | boolean (getter) | true if pendingVersion > activeVersion |


🛡️ Force Update

For P0 incidents where deferral is not an option:

// updater.config.ts
export default defineConfig({
  outDir: './dist',
  forceUpdate: 'prompt'  // or 'silent'
})

| Mode | Behavior | |---|---| | 'prompt' | Native OS warning dialog with only one button ("Confirm"). No "Later" option. Download starts immediately. | | 'silent' | Zero UI. Downloads and restarts entirely in the background. |

⚠️ Use only for genuine P0 incidents. This permanently removes the user's ability to defer.


🔐 RSA Signature Verification (Optional)

# Generate key pair
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
// updater.config.ts
export default defineConfig({ outDir: './dist', privateKeyPath: './private.pem' })

// main/index.ts
import { readFileSync } from 'fs'
new RenderUpdater({
  updateUrl: '...',
  versionsDir: '...',
  publicKey: readFileSync('./public.pem', 'utf-8')
})

License

MIT