diff --git a/packages/electron-updater/src/AppUpdater.ts b/packages/electron-updater/src/AppUpdater.ts index 183e3bae3ba..4ada45eed5e 100644 --- a/packages/electron-updater/src/AppUpdater.ts +++ b/packages/electron-updater/src/AppUpdater.ts @@ -6,7 +6,7 @@ import { EventEmitter } from "events" import { readFile } from "fs-extra-p" import { safeLoad } from "js-yaml" import * as path from "path" -import { gt as isVersionGreaterThan, valid as parseVersion } from "semver" +import { eq as isVersionsEqual, gt as isVersionGreaterThan, prerelease as getVersionPreleaseComponents, valid as parseVersion } from "semver" import "source-map-support/register" import { FileInfo, Provider, UpdateCheckResult, UpdaterSignal } from "./api" import { BintrayProvider } from "./BintrayProvider" @@ -25,10 +25,23 @@ export interface Logger { export abstract class AppUpdater extends EventEmitter { /** - * Automatically download an update when it is found. + * Whether to automatically download an update when it is found. */ autoDownload = true + /** + * *GitHub provider only.* Whether to allow update to pre-release versions. Defaults to `true` if application version contains prerelease components (e.g. `0.12.1-alpha.1`, here `alpha` is a prerelease component), otherwise `false`. + * + * If `true`, downgrade will be allowed (`allowDowngrade` will be set to `true`). + */ + allowPrerelease = false + + /** + * Whether to allow version downgrade (when a user from the beta channel wants to go back to the stable channel). + * Defaults to `true` if application version contains prerelease components (e.g. `0.12.1-alpha.1`, here `alpha` is a prerelease component), otherwise `false`. + */ + allowDowngrade = false + /** * The request headers. */ @@ -64,7 +77,9 @@ export abstract class AppUpdater extends EventEmitter { protected versionInfo: VersionInfo | null private fileInfo: FileInfo | null - constructor(options: PublishConfiguration | null | undefined) { + private currentVersion: string + + constructor(options: PublishConfiguration | null | undefined, app?: any) { super() this.on("error", (error: Error) => { @@ -73,8 +88,8 @@ export abstract class AppUpdater extends EventEmitter { } }) - if ((global).__test_app != null) { - this.app = (global).__test_app + if (app != null || (global).__test_app != null) { + this.app = app || (global).__test_app this.untilAppReady = BluebirdPromise.resolve() } else { @@ -96,6 +111,16 @@ export abstract class AppUpdater extends EventEmitter { }) } + const currentVersionString = this.app.getVersion() + this.currentVersion = parseVersion(currentVersionString) + if (this.currentVersion == null) { + throw new Error(`App version is not valid semver version: "${currentVersionString}`) + } + + const versionPrereleaseComponent = getVersionPreleaseComponents(this.currentVersion) + this.allowDowngrade = versionPrereleaseComponent != null && versionPrereleaseComponent.length > 0 + this.allowPrerelease = this.allowDowngrade + if (options != null) { this.setFeedURL(options) } @@ -171,16 +196,10 @@ export abstract class AppUpdater extends EventEmitter { throw new Error(`Latest version (from update server) is not valid semver version: "${latestVersion}`) } - const currentVersionString = this.app.getVersion() - const currentVersion = parseVersion(currentVersionString) - if (currentVersion == null) { - throw new Error(`App version is not valid semver version: "${currentVersion}`) - } - - if (!isVersionGreaterThan(latestVersion, currentVersion)) { + if (this.allowDowngrade ? isVersionsEqual(latestVersion, this.currentVersion) : !isVersionGreaterThan(latestVersion, this.currentVersion)) { this.updateAvailable = false if (this.logger != null) { - this.logger.info(`Update for version ${currentVersionString} is not available (latest version: ${versionInfo.version})`) + this.logger.info(`Update for version ${this.currentVersion} is not available (latest version: ${versionInfo.version}, downgrade is ${this.allowDowngrade ? "allowed" : "disallowed"}.`) } this.emit("update-not-available", versionInfo) return { diff --git a/packages/electron-updater/src/NsisUpdater.ts b/packages/electron-updater/src/NsisUpdater.ts index 7026611095a..6c77bb3cc6d 100644 --- a/packages/electron-updater/src/NsisUpdater.ts +++ b/packages/electron-updater/src/NsisUpdater.ts @@ -14,8 +14,8 @@ export class NsisUpdater extends AppUpdater { private quitAndInstallCalled = false private quitHandlerAdded = false - constructor(options?: PublishConfiguration) { - super(options) + constructor(options?: PublishConfiguration, app?: any) { + super(options, app) } /** diff --git a/test/out/__snapshots__/nsisUpdaterTest.js.snap b/test/out/__snapshots__/nsisUpdaterTest.js.snap index 18e47ca90d9..c80c04472f6 100644 --- a/test/out/__snapshots__/nsisUpdaterTest.js.snap +++ b/test/out/__snapshots__/nsisUpdaterTest.js.snap @@ -20,6 +20,16 @@ Array [ ] `; +exports[`downgrade (allowed) 1`] = ` +Object { + "name": "TestApp Setup 1.1.0.exe", + "sha2": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", + "url": "https://dl.bintray.com/actperepo/generic/TestApp Setup 1.1.0.exe", +} +`; + +exports[`downgrade (disallowed) 1`] = `undefined`; + exports[`file url 1`] = ` Object { "name": "TestApp Setup 1.1.0.exe", diff --git a/test/src/nsisUpdaterTest.ts b/test/src/nsisUpdaterTest.ts index e2bd0cc2c34..76ab027bb41 100644 --- a/test/src/nsisUpdaterTest.ts +++ b/test/src/nsisUpdaterTest.ts @@ -41,18 +41,8 @@ test("check updates - no versions at all", async () => { await assertThat(updater.checkForUpdates()).throws() }) -// test("cannot find suitable file for version", async () => { -// const updater = new NsisUpdater({ -// provider: "bintray", -// owner: "actperepo", -// package: "incorrect-file-version", -// }) -// -// await assertThat(updater.checkForUpdates()).throws() -// }) - -test("file url", async () => { - const updater = new NsisUpdater() +async function testUpdateFromBintray(app: any) { + const updater = new NsisUpdater(null, app) updater.updateConfigPath = await writeUpdateConfig({ provider: "bintray", owner: "actperepo", @@ -71,9 +61,58 @@ test("file url", async () => { expect(updateCheckResult.fileInfo).toMatchSnapshot() await assertThat(path.join(await updateCheckResult.downloadPromise)).isFile() + expect(actualEvents).toEqual(expectedEvents) +} +test("file url", () => testUpdateFromBintray(null)) + +test("downgrade (disallowed)", async () => { + const updater = new NsisUpdater(null, { + getVersion: function () { + return "2.0.0" + }, + + getAppPath: function () { + }, + + on: function () { + // ignored + }, + } + ) + updater.updateConfigPath = await writeUpdateConfig({ + provider: "bintray", + owner: "actperepo", + package: "TestApp", + }) + + const actualEvents: Array = [] + const expectedEvents = ["checking-for-update", "update-not-available"] + for (const eventName of expectedEvents) { + updater.addListener(eventName, () => { + actualEvents.push(eventName) + }) + } + + const updateCheckResult = await updater.checkForUpdates() + expect(updateCheckResult.fileInfo).toMatchSnapshot() + expect(updateCheckResult.downloadPromise).toBeUndefined() + expect(actualEvents).toEqual(expectedEvents) }) +test("downgrade (allowed)", () => testUpdateFromBintray({ + getVersion: function () { + return "2.0.0-beta.1" + }, + + getAppPath: function () { + }, + + on: function () { + // ignored + }, +})) + test("file url generic", async () => { const updater = new NsisUpdater() updater.updateConfigPath = await writeUpdateConfig({