@capgo/capacitor-updater 8.51.25 → 8.52.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapgoCapacitorUpdater.podspec +0 -3
- package/Package.swift +3 -11
- package/README.md +4 -1
- package/android/build.gradle +0 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +182 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +114 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +0 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +80 -21
- package/android/src/main/java/ee/forgr/capacitor_updater/Version.java +306 -0
- package/dist/docs.json +16 -0
- package/dist/esm/definitions.d.ts +14 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +90 -34
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoSemanticVersion.swift +162 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +218 -71
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +3 -4
- package/ios/Sources/CapacitorUpdaterPlugin/RedirectPolicyDelegate.swift +53 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ZipArchiveReader.swift +247 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ZipCentralDirectory.swift +280 -0
- package/package.json +1 -1
|
@@ -8,7 +8,6 @@ import Foundation
|
|
|
8
8
|
import Capacitor
|
|
9
9
|
import UIKit
|
|
10
10
|
import WebKit
|
|
11
|
-
import Version
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Please read the Capacitor iOS Plugin Development Guide
|
|
@@ -96,7 +95,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
96
95
|
deinit {
|
|
97
96
|
implementation.shutdown()
|
|
98
97
|
}
|
|
99
|
-
private let pluginVersion: String = "8.
|
|
98
|
+
private let pluginVersion: String = "8.52.1"
|
|
100
99
|
private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
101
100
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
102
101
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
@@ -147,7 +146,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
147
146
|
// Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
|
|
148
147
|
private var updateUrl = ""
|
|
149
148
|
private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
|
|
150
|
-
private var currentVersionNative:
|
|
149
|
+
private var currentVersionNative = CapgoSemanticVersion(major: 0, minor: 0, patch: 0)
|
|
151
150
|
private var currentBuildVersion: String = "0"
|
|
152
151
|
private var autoUpdate = false
|
|
153
152
|
private var autoUpdateMode = CapacitorUpdaterPlugin.autoUpdateModeOff
|
|
@@ -219,6 +218,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
219
218
|
// Best-effort flag: set before we expect notifyAppReady (load/reload).
|
|
220
219
|
// No lock — never held across waits; a rare race only mis-times one wait.
|
|
221
220
|
private var pendingNotifyAppReady = false
|
|
221
|
+
// Armed only after a reload. The next document stamps this generation into notifyAppReady.
|
|
222
|
+
// A call from the previous page has no matching generation and must not mark the new bundle successful.
|
|
223
|
+
private var readyGuardArmed = false
|
|
224
|
+
private var readyGeneration = 0
|
|
222
225
|
private let semaphoreWaitTestingLock = NSLock()
|
|
223
226
|
private var didEnterSemaphoreWaitForTesting = false
|
|
224
227
|
|
|
@@ -264,7 +267,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
264
267
|
fatalError("Cannot get version name")
|
|
265
268
|
}
|
|
266
269
|
do {
|
|
267
|
-
currentVersionNative = try
|
|
270
|
+
currentVersionNative = try CapgoSemanticVersion(versionName)
|
|
268
271
|
} catch {
|
|
269
272
|
logger.error("Cannot parse versionName \(versionName)")
|
|
270
273
|
}
|
|
@@ -288,6 +291,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
288
291
|
configureAutoUpdateModeFromConfig()
|
|
289
292
|
appReadyTimeout = max(1000, getConfig().getInt("appReadyTimeout", 10000)) // Minimum 1 second
|
|
290
293
|
implementation.timeout = Double(getConfig().getInt("responseTimeout", 20))
|
|
294
|
+
implementation.allowHttpsToHttpRedirect = getConfig().getBoolean("allowHttpsToHttpRedirect", false)
|
|
291
295
|
resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
|
|
292
296
|
shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
|
|
293
297
|
shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
@@ -308,16 +312,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
308
312
|
implementation.setPublicKey(getConfig().getString("publicKey") ?? "")
|
|
309
313
|
implementation.notifyDownloadRaw = notifyDownload
|
|
310
314
|
implementation.notifyListeners = { [weak self] eventName, data in
|
|
311
|
-
|
|
312
|
-
self?.notifyListeners(eventName, data: data)
|
|
313
|
-
}
|
|
314
|
-
if Thread.isMainThread {
|
|
315
|
-
emit()
|
|
316
|
-
} else {
|
|
317
|
-
DispatchQueue.main.async {
|
|
318
|
-
emit()
|
|
319
|
-
}
|
|
320
|
-
}
|
|
315
|
+
self?.notifyListenersOnMain(eventName, data: data)
|
|
321
316
|
}
|
|
322
317
|
implementation.pluginVersion = self.pluginVersion
|
|
323
318
|
|
|
@@ -1244,9 +1239,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1244
1239
|
call.keepAlive = true
|
|
1245
1240
|
}
|
|
1246
1241
|
|
|
1247
|
-
|
|
1242
|
+
/// CAPPlugin's listener storage is not thread-safe (ionic-team/capacitor#8157), so every
|
|
1243
|
+
/// event this plugin emits goes through the main thread. Calls already on main stay synchronous.
|
|
1244
|
+
private func notifyListenersOnMain(_ eventName: String, data: [String: Any]?, retainUntilConsumed: Bool = false) {
|
|
1248
1245
|
let notify = {
|
|
1249
|
-
self.notifyListeners(eventName, data: data)
|
|
1246
|
+
self.notifyListeners(eventName, data: data, retainUntilConsumed: retainUntilConsumed)
|
|
1250
1247
|
}
|
|
1251
1248
|
|
|
1252
1249
|
if Thread.isMainThread {
|
|
@@ -1716,6 +1713,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1716
1713
|
self.logger.error("Cannot get capBridge")
|
|
1717
1714
|
return false
|
|
1718
1715
|
}
|
|
1716
|
+
self.armReadyGuard(webView: vc.webView)
|
|
1719
1717
|
if self.keepUrlPathAfterReload {
|
|
1720
1718
|
if let currentURL = vc.webView?.url {
|
|
1721
1719
|
capBridge.setServerBasePath(dest.path)
|
|
@@ -1767,7 +1765,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1767
1765
|
return false
|
|
1768
1766
|
}
|
|
1769
1767
|
self.checkAppReady()
|
|
1770
|
-
self.
|
|
1768
|
+
self.notifyListenersOnMain("appReloaded", data: [:])
|
|
1771
1769
|
return true
|
|
1772
1770
|
}
|
|
1773
1771
|
|
|
@@ -1790,7 +1788,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1790
1788
|
return false
|
|
1791
1789
|
}
|
|
1792
1790
|
self.checkAppReady()
|
|
1793
|
-
self.
|
|
1791
|
+
self.notifyListenersOnMain("appReloaded", data: [:])
|
|
1794
1792
|
return true
|
|
1795
1793
|
}
|
|
1796
1794
|
|
|
@@ -2705,12 +2703,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2705
2703
|
var response: URLResponse?
|
|
2706
2704
|
var responseError: Error?
|
|
2707
2705
|
|
|
2708
|
-
|
|
2706
|
+
self.implementation.startRawDataTask(request) { data, urlResponse, error in
|
|
2709
2707
|
responseData = data
|
|
2710
2708
|
response = urlResponse
|
|
2711
2709
|
responseError = error
|
|
2712
2710
|
semaphore.signal()
|
|
2713
|
-
}
|
|
2711
|
+
}
|
|
2714
2712
|
|
|
2715
2713
|
if semaphore.wait(timeout: .now() + 60) == .timedOut {
|
|
2716
2714
|
throw makePreviewError("Preview payload request timed out")
|
|
@@ -3325,7 +3323,65 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3325
3323
|
)
|
|
3326
3324
|
}
|
|
3327
3325
|
|
|
3326
|
+
static func shouldAcceptReadyCall(guardArmed: Bool, expectedGeneration: Int, reportedGeneration: Int?) -> Bool {
|
|
3327
|
+
if !guardArmed {
|
|
3328
|
+
return true
|
|
3329
|
+
}
|
|
3330
|
+
guard let reportedGeneration else {
|
|
3331
|
+
return false
|
|
3332
|
+
}
|
|
3333
|
+
return reportedGeneration == expectedGeneration
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
static func readyGenerationScript(_ generation: Int) -> String {
|
|
3337
|
+
// Wrap Capacitor.nativePromise, not the plugin proxy. registerPlugin's get trap
|
|
3338
|
+
// ignores assignments to notifyAppReady. Each document keeps its own generation.
|
|
3339
|
+
return "(function(){window.__CAPGO_READY_GEN=\(generation);if(window.__capgoReadyBridge)return;function arm(){var cap=window.Capacitor;if(!cap||typeof cap.nativePromise!=='function'||cap.__capgoNativePromise)return false;var orig=cap.nativePromise.bind(cap);cap.nativePromise=function(pluginName,methodName,options){if(pluginName==='CapacitorUpdater'&&methodName==='notifyAppReady'){var next={};if(options&&typeof options==='object'){for(var k in options){if(Object.prototype.hasOwnProperty.call(options,k))next[k]=options[k];}}next.loadGeneration=window.__CAPGO_READY_GEN;options=next;}return orig(pluginName,methodName,options);};cap.__capgoNativePromise=true;window.__capgoReadyBridge=true;return true;}if(!arm()){var n=0;var t=setInterval(function(){if(arm()||++n>100)clearInterval(t);},20);}})();"
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
private func armReadyGuard(webView: WKWebView?) {
|
|
3343
|
+
let generation = self.readyGeneration + 1
|
|
3344
|
+
guard let webView else {
|
|
3345
|
+
logger.warn("Cannot stamp notifyAppReady generation without a webview")
|
|
3346
|
+
self.readyGuardArmed = false
|
|
3347
|
+
return
|
|
3348
|
+
}
|
|
3349
|
+
let userScript = WKUserScript(
|
|
3350
|
+
source: CapacitorUpdaterPlugin.readyGenerationScript(generation),
|
|
3351
|
+
injectionTime: .atDocumentStart,
|
|
3352
|
+
forMainFrameOnly: true
|
|
3353
|
+
)
|
|
3354
|
+
webView.configuration.userContentController.addUserScript(userScript)
|
|
3355
|
+
self.readyGeneration = generation
|
|
3356
|
+
self.readyGuardArmed = true
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
private func reportedReadyGeneration(_ call: CAPPluginCall) -> Int? {
|
|
3360
|
+
if let generation = call.getInt("loadGeneration") {
|
|
3361
|
+
return generation
|
|
3362
|
+
}
|
|
3363
|
+
// The bridge often boxes JS numbers as NSNumber, which getInt does not cast.
|
|
3364
|
+
if let number = call.getValue("loadGeneration") as? NSNumber {
|
|
3365
|
+
return number.intValue
|
|
3366
|
+
}
|
|
3367
|
+
return nil
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
private func acceptsReadyCall(_ call: CAPPluginCall) -> Bool {
|
|
3371
|
+
let reported = self.readyGuardArmed ? self.reportedReadyGeneration(call) : nil
|
|
3372
|
+
return CapacitorUpdaterPlugin.shouldAcceptReadyCall(
|
|
3373
|
+
guardArmed: self.readyGuardArmed,
|
|
3374
|
+
expectedGeneration: self.readyGeneration,
|
|
3375
|
+
reportedGeneration: reported
|
|
3376
|
+
)
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3328
3379
|
@objc func notifyAppReady(_ call: CAPPluginCall) {
|
|
3380
|
+
if !self.acceptsReadyCall(call) {
|
|
3381
|
+
logger.info("Ignoring notifyAppReady from a page that is no longer current")
|
|
3382
|
+
call.resolve(["bundle": self.implementation.getCurrentBundle().toJSON()])
|
|
3383
|
+
return
|
|
3384
|
+
}
|
|
3329
3385
|
self.semaphoreDown()
|
|
3330
3386
|
let bundle = self.implementation.getCurrentBundle()
|
|
3331
3387
|
self.implementation.setSuccess(bundle: bundle, autoDeletePrevious: self.autoDeletePrevious)
|
|
@@ -3433,7 +3489,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3433
3489
|
if BundleStatus.SUCCESS.storedValue != current.getStatus() {
|
|
3434
3490
|
logger.error("notifyAppReady was not called, roll back current bundle: \(current.toString())")
|
|
3435
3491
|
logger.error("Did you forget to call 'notifyAppReady()' in your Capacitor App code?")
|
|
3436
|
-
self.
|
|
3492
|
+
self.notifyListenersOnMain("updateFailed", data: [
|
|
3437
3493
|
"bundle": current.toJSON()
|
|
3438
3494
|
])
|
|
3439
3495
|
self.persistLastFailedBundle(current)
|
|
@@ -3475,7 +3531,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3475
3531
|
}
|
|
3476
3532
|
|
|
3477
3533
|
private func notifyBundleSet(_ bundle: BundleInfo) {
|
|
3478
|
-
self.
|
|
3534
|
+
self.notifyListenersOnMain("set", data: ["bundle": bundle.toJSON()], retainUntilConsumed: true)
|
|
3479
3535
|
}
|
|
3480
3536
|
|
|
3481
3537
|
func sendReadyToJs(current: BundleInfo, msg: String) {
|
|
@@ -3487,7 +3543,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3487
3543
|
if self.consumePendingNotifyAppReady() {
|
|
3488
3544
|
self.semaphoreWait(waitTime: self.appReadyTimeout)
|
|
3489
3545
|
}
|
|
3490
|
-
self.
|
|
3546
|
+
self.notifyListenersOnMain("appReady", data: ["bundle": current.toJSON(), "status": msg], retainUntilConsumed: true)
|
|
3491
3547
|
|
|
3492
3548
|
// Auto hide splashscreen if enabled
|
|
3493
3549
|
// We show it on background when conditions are met, so we should hide it on foreground regardless of update outcome
|
|
@@ -4263,8 +4319,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4263
4319
|
return
|
|
4264
4320
|
}
|
|
4265
4321
|
let payload: [String: Any] = ["version": version]
|
|
4266
|
-
self.
|
|
4267
|
-
self.
|
|
4322
|
+
self.notifyListenersOnMain("breakingAvailable", data: payload)
|
|
4323
|
+
self.notifyListenersOnMain("majorAvailable", data: payload)
|
|
4268
4324
|
}
|
|
4269
4325
|
|
|
4270
4326
|
private func shouldNotifyBreakingEvents(response: AppVersion) -> Bool {
|
|
@@ -4304,7 +4360,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4304
4360
|
let responseMessage = res.message?.isEmpty == false ? res.message : nil
|
|
4305
4361
|
let message = responseMessage ?? (backendError.isEmpty ? "server did not provide a message" : backendError)
|
|
4306
4362
|
let latestVersionName = res.version.isEmpty ? current.getVersionName() : res.version
|
|
4307
|
-
self.
|
|
4363
|
+
self.notifyListenersOnMain("updateCheckResult", data: [
|
|
4308
4364
|
"kind": responseKind,
|
|
4309
4365
|
"error": backendError,
|
|
4310
4366
|
"message": message,
|
|
@@ -4357,10 +4413,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4357
4413
|
if sendStats {
|
|
4358
4414
|
self.implementation.sendStats(action: failureAction, versionName: current.getVersionName())
|
|
4359
4415
|
}
|
|
4360
|
-
self.
|
|
4416
|
+
self.notifyListenersOnMain(failureEvent, data: ["version": latestVersionName])
|
|
4361
4417
|
}
|
|
4362
4418
|
if notifyNoNeedUpdate {
|
|
4363
|
-
self.
|
|
4419
|
+
self.notifyListenersOnMain("noNeedUpdate", data: ["bundle": current.toJSON()])
|
|
4364
4420
|
}
|
|
4365
4421
|
self.sendReadyToJs(current: current, msg: msg)
|
|
4366
4422
|
logger.info("endBackGroundTaskWithNotif \(msg) current: \(current.getVersionName()) latestVersionName: \(latestVersionName)")
|
|
@@ -4517,7 +4573,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4517
4573
|
let builtinUpdateAvailable = !current.isBuiltin()
|
|
4518
4574
|
if builtinUpdateAvailable {
|
|
4519
4575
|
let builtinBundle = self.implementation.getBundleInfo(id: BundleInfo.ID_BUILTIN)
|
|
4520
|
-
self.
|
|
4576
|
+
self.notifyListenersOnMain("updateAvailable", data: ["bundle": builtinBundle.toJSON()], retainUntilConsumed: true)
|
|
4521
4577
|
}
|
|
4522
4578
|
self.endBackGroundTaskWithNotif(
|
|
4523
4579
|
msg: "Latest version is builtin, autoUpdate onlyDownload",
|
|
@@ -4668,7 +4724,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4668
4724
|
plannedDirectUpdate: plannedDirectUpdate
|
|
4669
4725
|
)
|
|
4670
4726
|
} else if self.queueBundleForNextBackgroundInstall(next) {
|
|
4671
|
-
self.
|
|
4727
|
+
self.notifyListenersOnMain("updateAvailable", data: ["bundle": next.toJSON()])
|
|
4672
4728
|
self.endBackGroundTaskWithNotif(
|
|
4673
4729
|
msg: "Direct update reload failed, update will install next background",
|
|
4674
4730
|
latestVersionName: latestVersionName,
|
|
@@ -4689,7 +4745,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4689
4745
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will install on next app background.")
|
|
4690
4746
|
}
|
|
4691
4747
|
if self.queueBundleForNextBackgroundInstall(next) {
|
|
4692
|
-
self.
|
|
4748
|
+
self.notifyListenersOnMain("updateAvailable", data: ["bundle": next.toJSON()])
|
|
4693
4749
|
self.endBackGroundTaskWithNotif(
|
|
4694
4750
|
msg: "update downloaded, will install next background",
|
|
4695
4751
|
latestVersionName: latestVersionName,
|
|
@@ -4707,7 +4763,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4707
4763
|
}
|
|
4708
4764
|
} else {
|
|
4709
4765
|
self.logger.info("autoUpdate is set to onlyDownload, downloaded update will not be set as next bundle")
|
|
4710
|
-
self.
|
|
4766
|
+
self.notifyListenersOnMain("updateAvailable", data: ["bundle": next.toJSON()], retainUntilConsumed: true)
|
|
4711
4767
|
self.endBackGroundTaskWithNotif(
|
|
4712
4768
|
msg: "update downloaded, autoUpdate onlyDownload",
|
|
4713
4769
|
latestVersionName: latestVersionName,
|
|
@@ -5052,8 +5108,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
5052
5108
|
// Determine update availability by comparing versions
|
|
5053
5109
|
if let availableVersion = availableVersion {
|
|
5054
5110
|
do {
|
|
5055
|
-
let currentVer = try
|
|
5056
|
-
let availableVer = try
|
|
5111
|
+
let currentVer = try CapgoSemanticVersion(currentVersionName)
|
|
5112
|
+
let availableVer = try CapgoSemanticVersion(availableVersion)
|
|
5057
5113
|
if availableVer > currentVer {
|
|
5058
5114
|
result["updateAvailability"] = AppUpdateAvailability.updateAvailable.rawValue
|
|
5059
5115
|
} else {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Foundation
|
|
8
|
+
|
|
9
|
+
/// Minimal version value used for native version comparisons.
|
|
10
|
+
///
|
|
11
|
+
/// Parsing and ordering intentionally mirror the lenient parser of the `Version` package
|
|
12
|
+
/// (mrackwitz/Version 0.8.0) that this type replaces, so existing comparisons keep the same results:
|
|
13
|
+
/// - `MAJOR[.MINOR[.PATCH]][-PRERELEASE][+BUILD]`, numeric components are ASCII digits (leading zeros allowed).
|
|
14
|
+
/// - Missing minor/patch compare as `0`.
|
|
15
|
+
/// - A version with a prerelease is lower than the same version without one.
|
|
16
|
+
/// - Prerelease identifiers are compared dot by dot: numerically when both are digits, lexically otherwise.
|
|
17
|
+
/// - Build metadata is ignored for equality and ordering.
|
|
18
|
+
public struct CapgoSemanticVersion: Comparable, Hashable, CustomStringConvertible {
|
|
19
|
+
public enum ParseError: Error {
|
|
20
|
+
case invalidComponents
|
|
21
|
+
case invalidMajorComponent
|
|
22
|
+
case invalidMinorComponent
|
|
23
|
+
case invalidPatchComponent
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public let major: Int
|
|
27
|
+
public let minor: Int?
|
|
28
|
+
public let patch: Int?
|
|
29
|
+
public let prerelease: String?
|
|
30
|
+
public let build: String?
|
|
31
|
+
|
|
32
|
+
public var canonicalMinor: Int { minor ?? 0 }
|
|
33
|
+
public var canonicalPatch: Int { patch ?? 0 }
|
|
34
|
+
|
|
35
|
+
// Same pattern as the lenient `Version` parser (including its build group quirk: no dots in build metadata).
|
|
36
|
+
private static let versionPattern =
|
|
37
|
+
"\\A([0-9]+)(?:\\.([0-9]+))?(?:\\.([0-9]+))?(?:-([0-9A-Za-z-.]+))?(?:\\+([0-9A-Za-z-]+))??\\z"
|
|
38
|
+
private static let versionRegex = try? NSRegularExpression(pattern: versionPattern)
|
|
39
|
+
|
|
40
|
+
public init(major: Int, minor: Int? = nil, patch: Int? = nil, prerelease: String? = nil, build: String? = nil) {
|
|
41
|
+
self.major = max(0, major)
|
|
42
|
+
self.minor = minor.map { max(0, $0) }
|
|
43
|
+
self.patch = patch.map { max(0, $0) }
|
|
44
|
+
self.prerelease = prerelease
|
|
45
|
+
self.build = build
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public init(_ string: String) throws {
|
|
49
|
+
guard let regex = Self.versionRegex else {
|
|
50
|
+
throw ParseError.invalidComponents
|
|
51
|
+
}
|
|
52
|
+
let nsString = string as NSString
|
|
53
|
+
guard let match = regex.firstMatch(in: string, options: [], range: NSRange(location: 0, length: nsString.length)),
|
|
54
|
+
match.numberOfRanges == 6 else {
|
|
55
|
+
throw ParseError.invalidComponents
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func group(_ index: Int) -> String? {
|
|
59
|
+
let range = match.range(at: index)
|
|
60
|
+
return range.location == NSNotFound ? nil : nsString.substring(with: range)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
guard let major = group(1).flatMap({ Int($0) }) else {
|
|
64
|
+
throw ParseError.invalidMajorComponent
|
|
65
|
+
}
|
|
66
|
+
let minorComponent = group(2)
|
|
67
|
+
let minor = minorComponent.flatMap { Int($0) }
|
|
68
|
+
if minorComponent != nil && minor == nil {
|
|
69
|
+
throw ParseError.invalidMinorComponent
|
|
70
|
+
}
|
|
71
|
+
let patchComponent = group(3)
|
|
72
|
+
let patch = patchComponent.flatMap { Int($0) }
|
|
73
|
+
if patchComponent != nil && patch == nil {
|
|
74
|
+
throw ParseError.invalidPatchComponent
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
self.major = major
|
|
78
|
+
self.minor = minor
|
|
79
|
+
self.patch = patch
|
|
80
|
+
self.prerelease = group(4)
|
|
81
|
+
self.build = group(5)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public var description: String {
|
|
85
|
+
var result = "\(major)"
|
|
86
|
+
if let minor {
|
|
87
|
+
result += ".\(minor)"
|
|
88
|
+
}
|
|
89
|
+
if let patch {
|
|
90
|
+
result += ".\(patch)"
|
|
91
|
+
}
|
|
92
|
+
if let prerelease {
|
|
93
|
+
result += "-\(prerelease)"
|
|
94
|
+
}
|
|
95
|
+
if let build {
|
|
96
|
+
result += "+\(build)"
|
|
97
|
+
}
|
|
98
|
+
return result
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public static func == (lhs: CapgoSemanticVersion, rhs: CapgoSemanticVersion) -> Bool {
|
|
102
|
+
return lhs.major == rhs.major
|
|
103
|
+
&& lhs.canonicalMinor == rhs.canonicalMinor
|
|
104
|
+
&& lhs.canonicalPatch == rhs.canonicalPatch
|
|
105
|
+
&& lhs.prerelease == rhs.prerelease
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public func hash(into hasher: inout Hasher) {
|
|
109
|
+
hasher.combine(major)
|
|
110
|
+
hasher.combine(canonicalMinor)
|
|
111
|
+
hasher.combine(canonicalPatch)
|
|
112
|
+
hasher.combine(prerelease)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public static func < (lhs: CapgoSemanticVersion, rhs: CapgoSemanticVersion) -> Bool {
|
|
116
|
+
if lhs.major != rhs.major {
|
|
117
|
+
return lhs.major < rhs.major
|
|
118
|
+
}
|
|
119
|
+
if lhs.canonicalMinor != rhs.canonicalMinor {
|
|
120
|
+
return lhs.canonicalMinor < rhs.canonicalMinor
|
|
121
|
+
}
|
|
122
|
+
if lhs.canonicalPatch != rhs.canonicalPatch {
|
|
123
|
+
return lhs.canonicalPatch < rhs.canonicalPatch
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
switch (lhs.prerelease, rhs.prerelease) {
|
|
127
|
+
case (.some, .none):
|
|
128
|
+
return true
|
|
129
|
+
case (.none, .some), (.none, .none):
|
|
130
|
+
return false
|
|
131
|
+
case let (.some(lpre), .some(rpre)):
|
|
132
|
+
return comparePrerelease(lpre, rpre) == .orderedAscending
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private static func isNumericIdentifier(_ value: String) -> Bool {
|
|
137
|
+
return !value.isEmpty && value.utf8.allSatisfy { $0 >= 0x30 && $0 <= 0x39 }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private static func comparePrerelease(_ lhs: String, _ rhs: String) -> ComparisonResult {
|
|
141
|
+
let lhsComponents = lhs.components(separatedBy: ".")
|
|
142
|
+
let rhsComponents = rhs.components(separatedBy: ".")
|
|
143
|
+
if let (left, right) = zip(lhsComponents, rhsComponents).first(where: { $0.0 != $0.1 }) {
|
|
144
|
+
if isNumericIdentifier(left) && isNumericIdentifier(right) {
|
|
145
|
+
let leftValue = Int(left) ?? 0
|
|
146
|
+
let rightValue = Int(right) ?? 0
|
|
147
|
+
if leftValue == rightValue {
|
|
148
|
+
return .orderedSame
|
|
149
|
+
}
|
|
150
|
+
return leftValue < rightValue ? .orderedAscending : .orderedDescending
|
|
151
|
+
}
|
|
152
|
+
if left == right {
|
|
153
|
+
return .orderedSame
|
|
154
|
+
}
|
|
155
|
+
return left < right ? .orderedAscending : .orderedDescending
|
|
156
|
+
}
|
|
157
|
+
if lhsComponents.count != rhsComponents.count {
|
|
158
|
+
return lhsComponents.count < rhsComponents.count ? .orderedAscending : .orderedDescending
|
|
159
|
+
}
|
|
160
|
+
return .orderedSame
|
|
161
|
+
}
|
|
162
|
+
}
|