basic-electron-updater
Version:
A secure, cross-platform auto-update library for Electron Forge apps using GitHub Releases.
287 lines (286 loc) • 12.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const electron_1 = require("electron");
const events_1 = require("./events");
const GitHubProvider_1 = require("./github/GitHubProvider");
const config_1 = require("./config");
const Downloader_1 = require("./download/Downloader");
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const windows_1 = require("./platform/windows");
const macos_1 = require("./platform/macos");
const linux_1 = require("./platform/linux");
const semver = __importStar(require("semver"));
/**
* Updater provides secure, cross-platform auto-update support for Electron Forge apps using GitHub Releases.
*
* @example
* import Updater from 'my-auto-updater';
* const updater = new Updater({ repo: 'user/repo' });
* updater.on('update-available', info => { ... });
* updater.checkForUpdates();
*/
class Updater extends events_1.TypedEventEmitter {
config;
githubProvider;
downloader;
lastUpdateInfo = null;
lastDownloadedPath = null;
currentVersion;
debug;
/**
* Create a new Updater instance.
* @param config Updater configuration options
*/
constructor(config) {
super();
this.config = (0, config_1.resolveConfig)(config);
this.githubProvider = new GitHubProvider_1.GitHubProvider({
repo: this.config.repo,
allowPrerelease: this.config.allowPrerelease,
channel: this.config.channel,
});
this.downloader = new Downloader_1.Downloader();
this.currentVersion = this.detectAppVersion();
this.debug = !!config.debug;
if (this.debug) {
this.sendDebugLog("[Updater:debug] Initialized with config:", this.config);
this.sendDebugLog("[Updater:debug] Current version:", this.currentVersion);
}
}
sendDebugLog(message, ...args) {
if (!this.debug)
return;
// Print to main process console
// eslint-disable-next-line no-console
console.debug(message, ...args);
// Send to all renderer processes if Electron IPC is available
try {
if (typeof electron_1.ipcMain !== "undefined" && electron_1.webContents.getAllWebContents) {
for (const wc of electron_1.webContents.getAllWebContents()) {
wc.send("basic-electron-updater:debug", message, ...args);
}
}
}
catch {
// Ignore errors when not in Electron context
}
}
detectAppVersion() {
try {
// Only works in Electron main process
// eslint-disable-next-line @typescript-eslint/no-var-requires
const electron = require("electron");
const version = electron.app.getVersion();
if (this.debug)
this.sendDebugLog("[Updater:debug] Detected Electron app version:", version);
return version;
}
catch {
if (this.debug)
this.sendDebugLog("[Updater:debug] Could not detect Electron app version, using 0.0.0");
return "0.0.0";
}
}
/**
* Checks GitHub Releases for the latest update.
* Emits 'update-available' or 'update-not-available'.
* @returns UpdateInfo if available, otherwise null
*/
async checkForUpdates() {
try {
this.emit("checking-for-update");
this.config.logger.info("Checking for updates...");
if (this.debug)
this.sendDebugLog("[Updater:debug] Calling getLatestRelease...");
const info = await this.githubProvider.getLatestRelease();
if (this.debug) {
this.sendDebugLog("[Updater:debug] Latest release info:", info);
this.sendDebugLog("[Updater:debug] Current version:", this.currentVersion);
}
this.lastUpdateInfo = info;
if (info && semver.valid(info.version) && semver.valid(this.currentVersion) &&
semver.gt(info.version, this.currentVersion)) {
if (this.debug)
this.sendDebugLog("[Updater:debug] Update available:", info.version);
this.emit("update-available", info);
this.config.logger.info("Update available:", info.version);
if (this.config.autoDownload) {
this.downloadUpdate();
}
return info;
}
else {
if (this.debug)
this.sendDebugLog("[Updater:debug] No update available.");
this.emit("update-not-available");
this.config.logger.info("No update available.");
return null;
}
}
catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
if (this.debug)
this.sendDebugLog("[Updater:debug] Error in checkForUpdates:", error);
this.emit("error", error);
this.config.logger.error("Update check failed:", error);
return null;
}
}
/**
* Downloads the update asset for the current platform.
* Emits 'download-progress' and 'downloaded'.
* Validates SHA256 and GPG signature if provided.
* @returns Path to the downloaded file
* @throws Error if download or validation fails
*/
async downloadUpdate() {
if (!this.lastUpdateInfo) {
throw new Error("No update info available. Call checkForUpdates() first.");
}
const platform = os.platform();
const arch = os.arch();
if (this.debug) {
this.sendDebugLog("[Updater:debug] Platform:", platform, "Arch:", arch);
this.sendDebugLog("[Updater:debug] Assets:", this.lastUpdateInfo.assets);
}
// Select asset for current platform
const asset = this.lastUpdateInfo.assets.find((a) => {
const n = a.name.toLowerCase();
if (platform === "win32") {
return n.endsWith(".exe") || n.endsWith(".msi") || n.endsWith(".nsis.zip") ||
n.includes("win") || n.includes("windows") || n.includes("setup");
}
if (platform === "darwin") {
return n.endsWith(".dmg") || n.endsWith(".zip") || n.endsWith(".pkg") ||
n.includes("mac") || n.includes("darwin") || n.includes("osx");
}
if (platform === "linux") {
return n.endsWith(".appimage") || n.endsWith(".tar.gz") || n.endsWith(".deb") ||
n.endsWith(".rpm") || n.endsWith(".snap") || n.includes("linux");
}
return false;
});
if (!asset)
throw new Error("No suitable asset found for platform: " + platform);
const dest = path.join(os.tmpdir(), asset.name);
if (this.debug) {
this.sendDebugLog("[Updater:debug] Downloading asset:", asset.url);
this.sendDebugLog("[Updater:debug] Download destination:", dest);
}
this.config.logger.info("Downloading update asset:", asset.url);
try {
const filePath = await this.downloader.downloadAsset(asset.url, dest, (progress) => {
if (this.debug)
this.sendDebugLog("[Updater:debug] Download progress:", progress);
this.emit("download-progress", progress);
}, asset.sha256);
// GPG signature validation if present
if (asset.gpgSignatureUrl) {
const sigDest = dest + ".sig";
if (this.debug)
this.sendDebugLog("[Updater:debug] Downloading GPG signature:", asset.gpgSignatureUrl);
await this.downloader.downloadAsset(asset.gpgSignatureUrl, sigDest, undefined, undefined);
try {
await this.downloader.validateGpg(filePath, sigDest);
if (this.debug)
this.sendDebugLog("[Updater:debug] GPG signature validated for", filePath);
this.config.logger.info("GPG signature validated for", filePath);
}
catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
if (this.debug)
this.sendDebugLog("[Updater:debug] GPG validation failed:", error);
this.emit("error", error);
this.config.logger.error("GPG validation failed:", error);
throw error;
}
}
this.emit("downloaded", filePath);
this.config.logger.info("Downloaded update to:", filePath);
this.lastDownloadedPath = filePath;
return filePath;
}
catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
if (this.debug)
this.sendDebugLog("[Updater:debug] Download failed:", error);
this.emit("error", error);
this.config.logger.error("Download failed:", error);
throw error;
}
}
/**
* Applies the downloaded update by launching the installer or archive.
* Handles platform-specific logic.
* @throws Error if no update is downloaded or launching fails
*/
async applyUpdate() {
if (!this.lastDownloadedPath) {
throw new Error("No downloaded update to apply. Call downloadUpdate() first.");
}
const platform = os.platform();
if (this.debug)
this.sendDebugLog("[Updater:debug] Applying update for platform:", platform);
this.config.logger.info("Applying update for platform:", platform);
try {
if (platform === "win32") {
await (0, windows_1.applyWindowsUpdate)(this.lastDownloadedPath);
}
else if (platform === "darwin") {
await (0, macos_1.applyMacUpdate)(this.lastDownloadedPath);
}
else if (platform === "linux") {
await (0, linux_1.applyLinuxUpdate)(this.lastDownloadedPath);
}
else {
throw new Error("Unsupported platform: " + platform);
}
if (this.debug)
this.sendDebugLog("[Updater:debug] Update applied (installer launched).");
this.config.logger.info("Update applied (installer launched).");
}
catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
if (this.debug)
this.sendDebugLog("[Updater:debug] Failed to apply update:", error);
this.emit("error", error);
this.config.logger.error("Failed to apply update:", error);
throw error;
}
}
}
exports.default = Updater;