expo-updates 57.0.14 → 57.0.15

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/CHANGELOG.md CHANGED
@@ -10,6 +10,10 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 57.0.15 — 2026-08-17
14
+
15
+ _This version does not introduce any user-facing changes._
16
+
13
17
  ## 57.0.14 — 2026-08-14
14
18
 
15
19
  ### 🐛 Bug fixes
@@ -42,7 +42,7 @@ expoModule {
42
42
  }
43
43
 
44
44
  group = 'host.exp.exponent'
45
- version = '57.0.14'
45
+ version = '57.0.15'
46
46
 
47
47
  // Utility method to derive boolean values from the environment or from Java properties,
48
48
  // and return them as strings to be used in BuildConfig fields
@@ -89,7 +89,7 @@ android {
89
89
  namespace "expo.modules.updates"
90
90
  defaultConfig {
91
91
  versionCode 31
92
- versionName '57.0.14'
92
+ versionName '57.0.15'
93
93
  consumerProguardFiles("proguard-rules.pro")
94
94
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
95
95
 
@@ -32,6 +32,11 @@ public class AppLauncherWithDatabase: NSObject, AppLauncher {
32
32
  public var launchAssetUrl: URL?
33
33
  public var assetFilesMap: [String: String]?
34
34
 
35
+ /// Set when the launched update is served directly from the embedded app binary. Tracked
36
+ /// explicitly because `assetFilesMap` is now populated with the bundle-resolved embedded assets,
37
+ /// so it can no longer double as the "using embedded assets" signal.
38
+ private var launchedFromEmbeddedBundle = false
39
+
35
40
  private let launcherQueue: DispatchQueue
36
41
  private var completedAssets: Int
37
42
  private let config: UpdatesConfig
@@ -54,7 +59,7 @@ public class AppLauncherWithDatabase: NSObject, AppLauncher {
54
59
  }
55
60
 
56
61
  public func isUsingEmbeddedAssets() -> Bool {
57
- return assetFilesMap == nil
62
+ return launchedFromEmbeddedBundle
58
63
  }
59
64
 
60
65
  public static func launchableUpdate(
@@ -178,7 +183,10 @@ public class AppLauncherWithDatabase: NSObject, AppLauncher {
178
183
  }
179
184
 
180
185
  if launchedUpdate.status == UpdateStatus.StatusEmbedded {
181
- precondition(assetFilesMap == nil, "assetFilesMap should be null for embedded updates")
186
+ launchedFromEmbeddedBundle = true
187
+ // The embedded update's assets aren't copied into the cache, so resolve them from the app
188
+ // binary. Populating the map here keeps `Updates.localAssets` available instead of empty.
189
+ assetFilesMap = UpdatesUtils.embeddedAssetsMap(withConfig: config, database: database, logger: logger)
182
190
  launchAssetUrl = updatesBundle.url(
183
191
  forResource: EmbeddedAppLoader.EXUpdatesBareEmbeddedBundleFilename,
184
192
  withExtension: EmbeddedAppLoader.EXUpdatesBareEmbeddedBundleFileType
@@ -8,14 +8,12 @@
8
8
  import Foundation
9
9
 
10
10
  /**
11
- * Subclass of AppLoader which handles copying the embedded update's assets into the
12
- * expo-updates cache location.
11
+ * Subclass of AppLoader which loads the embedded update.
13
12
  *
14
- * Rather than launching the embedded update directly from its location in the app bundle/apk, we
15
- * first try to read it into the expo-updates cache and database and launch it like any other
16
- * update. The benefits of this include (a) a single code path for launching most updates and (b)
17
- * assets included in embedded updates and copied into the cache in this way do not need to be
18
- * redownloaded if included in future updates.
13
+ * By default (`EX_UPDATES_COPY_EMBEDDED_ASSETS` off) the update is registered but its assets aren't
14
+ * read into the database; it keeps StatusEmbedded and launches from the app binary, so first launch
15
+ * stays fast. When the flag is on, assets are read into the database and it launches like any other
16
+ * update, so a later update reusing them avoids a redownload.
19
17
  */
20
18
  @objc(EXUpdatesEmbeddedAppLoader)
21
19
  @objcMembers
@@ -29,6 +27,24 @@ public final class EmbeddedAppLoader: AppLoader {
29
27
 
30
28
  private static var embeddedManifestInternal: EmbeddedUpdate?
31
29
 
30
+ /// Whether to read and hash embedded assets into the database on first launch. Defaults to the
31
+ /// build-time `EX_UPDATES_COPY_EMBEDDED_ASSETS` flag (off). Overridable for testing.
32
+ internal var shouldCopyEmbeddedAssets: Bool = UpdatesUtils.shouldCopyEmbeddedAssets()
33
+
34
+ internal let completionQueue: DispatchQueue
35
+
36
+ public required override init(
37
+ config: UpdatesConfig,
38
+ logger: UpdatesLogger,
39
+ database: UpdatesDatabase,
40
+ directory: URL,
41
+ launchedUpdate: Update?,
42
+ completionQueue: DispatchQueue
43
+ ) {
44
+ self.completionQueue = completionQueue
45
+ super.init(config: config, logger: logger, database: database, directory: directory, launchedUpdate: launchedUpdate, completionQueue: completionQueue)
46
+ }
47
+
32
48
  /**
33
49
  Gets the embedded update.
34
50
  If the `UpdatesConfig.hasEmbeddedUpdate` is false, it returns nil
@@ -136,6 +152,18 @@ public final class EmbeddedAppLoader: AppLoader {
136
152
  self.assetBlock = assetBlock
137
153
  self.successBlock = successBlock
138
154
  self.errorBlock = errorBlock
155
+ startEmbeddedLoad(fromEmbeddedManifest: embeddedManifest)
156
+ }
157
+
158
+ /// Loads the embedded update: with copying off (default) registers it without ingesting assets,
159
+ /// otherwise loads them like any other update. Separate from `loadUpdateResponseFromEmbeddedManifest`
160
+ /// so tests can pass a manifest without reading the bundle.
161
+ internal func startEmbeddedLoad(fromEmbeddedManifest embeddedManifest: Update) {
162
+ if !shouldCopyEmbeddedAssets {
163
+ registerEmbeddedUpdateWithoutCopying(embeddedManifest)
164
+ return
165
+ }
166
+
139
167
  startLoading(fromUpdateResponse: UpdateResponse(
140
168
  responseHeaderData: nil,
141
169
  manifestUpdateResponsePart: ManifestUpdateResponsePart(updateManifest: embeddedManifest),
@@ -143,6 +171,40 @@ public final class EmbeddedAppLoader: AppLoader {
143
171
  ))
144
172
  }
145
173
 
174
+ /// Registers the embedded update without reading, hashing, or copying its assets. A StatusEmbedded
175
+ /// update resolves its bundle and assets from the app binary at launch. Tradeoff: a future update
176
+ /// reusing an embedded asset re-downloads it.
177
+ private func registerEmbeddedUpdateWithoutCopying(_ embeddedManifest: Update) {
178
+ database.databaseQueue.async {
179
+ do {
180
+ let existingUpdate = try? self.database.update(withId: embeddedManifest.updateId, config: self.config)
181
+ if existingUpdate == nil {
182
+ try self.database.addUpdate(embeddedManifest, config: self.config)
183
+ }
184
+ // Mark finished (keep = 1) so the reaper retains it; it stays StatusEmbedded since copying is off.
185
+ try self.database.markUpdateFinished(embeddedManifest)
186
+ } catch {
187
+ let errorBlock = self.errorBlock
188
+ self.completionQueue.async {
189
+ errorBlock?(UpdatesError.appLoaderUnknownError(cause: error))
190
+ self.reset()
191
+ }
192
+ return
193
+ }
194
+
195
+ let successBlock = self.successBlock
196
+ let updateResponse = UpdateResponse(
197
+ responseHeaderData: nil,
198
+ manifestUpdateResponsePart: ManifestUpdateResponsePart(updateManifest: embeddedManifest),
199
+ directiveUpdateResponsePart: nil
200
+ )
201
+ self.completionQueue.async {
202
+ successBlock?(updateResponse)
203
+ self.reset()
204
+ }
205
+ }
206
+ }
207
+
146
208
  override public func downloadAsset(_ asset: UpdateAsset, extraHeaders: [String: Any]) {
147
209
  FileDownloader.assetFilesQueue.async {
148
210
  self.handleAssetDownloadAlreadyExists(asset)
@@ -280,7 +280,11 @@ public final class UpdatesDatabase: NSObject {
280
280
  }
281
281
 
282
282
  public func markUpdateFinished(_ update: Update) throws {
283
- if update.status != UpdateStatus.StatusDevelopment {
283
+ // With copying off, embedded updates have no asset rows, so they must stay StatusEmbedded to keep
284
+ // launching from the bundle. Promoting to StatusReady would route launch through the database
285
+ // asset path, which has no rows for them.
286
+ let keepEmbedded = update.status == UpdateStatus.StatusEmbedded && !UpdatesUtils.shouldCopyEmbeddedAssets()
287
+ if update.status != UpdateStatus.StatusDevelopment && !keepEmbedded {
284
288
  update.status = UpdateStatus.StatusReady
285
289
  }
286
290
 
@@ -182,6 +182,14 @@ public final class UpdatesUtils: NSObject {
182
182
  #endif
183
183
  }
184
184
 
185
+ internal static func shouldCopyEmbeddedAssets() -> Bool {
186
+ #if EX_UPDATES_COPY_EMBEDDED_ASSETS
187
+ return true
188
+ #else
189
+ return false
190
+ #endif
191
+ }
192
+
185
193
  internal static func runBlockOnMainThread(_ block: @escaping () -> Void) {
186
194
  if Thread.isMainThread {
187
195
  block()
@@ -9,6 +9,9 @@ end
9
9
  if ENV['EX_UPDATES_CUSTOM_INIT'] != '1'
10
10
  ENV['EX_UPDATES_CUSTOM_INIT'] = podfile_properties['updatesCustomInit'] == 'true' ? '1' : '0'
11
11
  end
12
+ if ENV['EX_UPDATES_COPY_EMBEDDED_ASSETS'] != '1'
13
+ ENV['EX_UPDATES_COPY_EMBEDDED_ASSETS'] = podfile_properties['updatesCopyEmbeddedAssets'] == 'true' ? '1' : '0'
14
+ end
12
15
 
13
16
  use_dev_client = false
14
17
  begin
@@ -72,6 +75,7 @@ Pod::Spec.new do |s|
72
75
 
73
76
  ex_updates_native_debug = ENV['EX_UPDATES_NATIVE_DEBUG'] == '1'
74
77
  ex_updates_custom_init = ENV['EX_UPDATES_CUSTOM_INIT'] == '1'
78
+ ex_updates_copy_embedded_assets = ENV['EX_UPDATES_COPY_EMBEDDED_ASSETS'] == '1'
75
79
  if ex_updates_native_debug
76
80
  other_debug_c_flags << ' -DEX_UPDATES_NATIVE_DEBUG=1'
77
81
  other_debug_swift_flags << ' -DEX_UPDATES_NATIVE_DEBUG'
@@ -82,6 +86,12 @@ Pod::Spec.new do |s|
82
86
  other_release_c_flags << ' -DEX_UPDATES_CUSTOM_INIT=1'
83
87
  other_release_swift_flags << ' -DEX_UPDATES_CUSTOM_INIT'
84
88
  end
89
+ if ex_updates_copy_embedded_assets
90
+ other_debug_c_flags << ' -DEX_UPDATES_COPY_EMBEDDED_ASSETS=1'
91
+ other_debug_swift_flags << ' -DEX_UPDATES_COPY_EMBEDDED_ASSETS'
92
+ other_release_c_flags << ' -DEX_UPDATES_COPY_EMBEDDED_ASSETS=1'
93
+ other_release_swift_flags << ' -DEX_UPDATES_COPY_EMBEDDED_ASSETS'
94
+ end
85
95
  if use_dev_client
86
96
  other_debug_c_flags << ' -DUSE_DEV_CLIENT=1'
87
97
  other_debug_swift_flags << ' -DUSE_DEV_CLIENT'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-updates",
3
- "version": "57.0.14",
3
+ "version": "57.0.15",
4
4
  "description": "Fetches and manages remotely-hosted assets and updates to your app's JS bundle.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -60,9 +60,9 @@
60
60
  "picomatch": "^4.0.4",
61
61
  "ts-node": "^10.9.2",
62
62
  "xstate": "^4.37.2",
63
+ "expo": "57.0.14",
63
64
  "@expo/metro-config": "57.0.8",
64
- "expo-dev-client": "57.0.12",
65
- "expo": "57.0.13",
65
+ "expo-dev-client": "57.0.13",
66
66
  "expo-module-scripts": "56.0.3"
67
67
  },
68
68
  "peerDependencies": {
@@ -82,7 +82,7 @@
82
82
  "./scripts/with-node.sh"
83
83
  ]
84
84
  },
85
- "gitHead": "125b8d4472432db3dc4ae047730432d7bd5bd30c",
85
+ "gitHead": "391d762fa5059cebf63ef2381da93994ce97fc63",
86
86
  "scripts": {
87
87
  "build": "expo-module build",
88
88
  "clean": "expo-module clean",