expo-updates 0.10.10 → 0.10.14

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,30 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 0.10.14 — 2021-11-09
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - Retain embedded asset fields when merging existing asset entities on Android. ([#15123](https://github.com/expo/expo/pull/15123) by [@esamelson](https://github.com/esamelson))
18
+
19
+ ## 0.10.13 — 2021-11-05
20
+
21
+ ### 🐛 Bug fixes
22
+
23
+ - Fix issue with assets that are duplicated in the local SQLite db being reaped when they are still in use. ([#15049](https://github.com/expo/expo/pull/15049) by [@esamelson](https://github.com/esamelson))
24
+
25
+ ## 0.10.12 — 2021-11-04
26
+
27
+ ### 🐛 Bug fixes
28
+
29
+ - Workaround for bridge being initialized twice on startup. ([#15019](https://github.com/expo/expo/pull/15019) by [@kudo](https://github.com/kudo))
30
+
31
+ ## 0.10.11 — 2021-11-02
32
+
33
+ ### 🐛 Bug fixes
34
+
35
+ - Fix handling of unexpectedly missing assets on iOS. ([#15008](https://github.com/expo/expo/pull/15008) by [@esamelson](https://github.com/esamelson))
36
+
13
37
  ## 0.10.10 — 2021-11-02
14
38
 
15
39
  _This version does not introduce any user-facing changes._
@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
3
3
  apply plugin: 'maven'
4
4
 
5
5
  group = 'host.exp.exponent'
6
- version = '0.10.10'
6
+ version = '0.10.14'
7
7
 
8
8
  apply from: "../scripts/create-manifest-android.gradle"
9
9
 
@@ -59,7 +59,7 @@ android {
59
59
  minSdkVersion safeExtGet("minSdkVersion", 21)
60
60
  targetSdkVersion safeExtGet("targetSdkVersion", 30)
61
61
  versionCode 31
62
- versionName '0.10.10'
62
+ versionName '0.10.14'
63
63
  consumerProguardFiles("proguard-rules.pro")
64
64
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
65
65
  // uncomment below to export the database schema when making changes
@@ -41,6 +41,12 @@ public abstract class AssetDao {
41
41
  " WHERE updates.keep);")
42
42
  public abstract void _unmarkUsedAssetsFromDeletion();
43
43
 
44
+ @Query("UPDATE assets SET marked_for_deletion = 0 WHERE relative_path IN (" +
45
+ " SELECT relative_path" +
46
+ " FROM assets" +
47
+ " WHERE marked_for_deletion = 0);")
48
+ public abstract void _unmarkDuplicateUsedAssetsFromDeletion();
49
+
44
50
  @Query("SELECT * FROM assets WHERE marked_for_deletion = 1;")
45
51
  public abstract List<AssetEntity> _loadAssetsMarkedForDeletion();
46
52
 
@@ -97,6 +103,12 @@ public abstract class AssetDao {
97
103
  }
98
104
  // we need to keep track of whether the calling class expects this asset to be the launch asset
99
105
  existingEntity.isLaunchAsset = newEntity.isLaunchAsset;
106
+ // some fields on the asset entity are not stored in the database but might still be used by application code
107
+ existingEntity.embeddedAssetFilename = newEntity.embeddedAssetFilename;
108
+ existingEntity.resourcesFilename = newEntity.resourcesFilename;
109
+ existingEntity.resourcesFolder = newEntity.resourcesFolder;
110
+ existingEntity.scale = newEntity.scale;
111
+ existingEntity.scales = newEntity.scales;
100
112
  }
101
113
 
102
114
  @Transaction
@@ -121,6 +133,8 @@ public abstract class AssetDao {
121
133
  // this is safe since this is a transaction and will be rolled back upon failure
122
134
  _markAllAssetsForDeletion();
123
135
  _unmarkUsedAssetsFromDeletion();
136
+ // check for duplicate rows representing a single file on disk
137
+ _unmarkDuplicateUsedAssetsFromDeletion();
124
138
 
125
139
  List<AssetEntity> deletedAssets = _loadAssetsMarkedForDeletion();
126
140
  _deleteAssetsMarkedForDeletion();
@@ -161,6 +161,8 @@ public class EmbeddedLoader {
161
161
 
162
162
  AssetEntity matchingDbEntry = mDatabase.assetDao().loadAssetWithKey(asset.key);
163
163
  if (matchingDbEntry != null) {
164
+ // merge all fields not stored in the database onto matchingDbEntry,
165
+ // in case we need them later on in this class
164
166
  mDatabase.assetDao().mergeAndUpdateAsset(matchingDbEntry, asset);
165
167
  asset = matchingDbEntry;
166
168
  }
@@ -183,6 +183,8 @@ public class RemoteLoader {
183
183
  for (AssetEntity assetEntity : assetList) {
184
184
  AssetEntity matchingDbEntry = mDatabase.assetDao().loadAssetWithKey(assetEntity.key);
185
185
  if (matchingDbEntry != null) {
186
+ // merge all fields not stored in the database onto matchingDbEntry,
187
+ // in case we need them later on in this class
186
188
  mDatabase.assetDao().mergeAndUpdateAsset(matchingDbEntry, assetEntity);
187
189
  assetEntity = matchingDbEntry;
188
190
  }
@@ -3,6 +3,7 @@
3
3
  #import <EXUpdates/EXUpdatesAsset.h>
4
4
  #import <EXUpdates/EXUpdatesAppLauncherNoDatabase.h>
5
5
  #import <EXUpdates/EXUpdatesEmbeddedAppLoader.h>
6
+ #import <EXUpdates/EXUpdatesUtils.h>
6
7
 
7
8
  NS_ASSUME_NONNULL_BEGIN
8
9
 
@@ -30,7 +31,7 @@ static NSString * const EXUpdatesErrorLogFile = @"expo-error.log";
30
31
 
31
32
  NSMutableDictionary *assetFilesMap = [NSMutableDictionary new];
32
33
  for (EXUpdatesAsset *asset in _launchedUpdate.assets) {
33
- NSURL *localUrl = [[NSBundle mainBundle] URLForResource:asset.mainBundleFilename withExtension:asset.type];
34
+ NSURL *localUrl = [EXUpdatesUtils urlForBundledAsset:asset];
34
35
  if (localUrl && asset.key) {
35
36
  assetFilesMap[asset.key] = localUrl.absoluteString;
36
37
  }
@@ -260,12 +260,14 @@ static NSString * const EXUpdatesAppLauncherErrorDomain = @"AppLauncher";
260
260
  }
261
261
 
262
262
  if (matchingAsset && matchingAsset.mainBundleFilename) {
263
- NSString *bundlePath = [[NSBundle mainBundle] pathForResource:matchingAsset.mainBundleFilename ofType:matchingAsset.type];
264
- if (bundlePath == nil) {
265
- completion(NO, nil);
266
- return;
267
- }
268
263
  dispatch_async([EXUpdatesFileDownloader assetFilesQueue], ^{
264
+ NSString *bundlePath = [EXUpdatesUtils pathForBundledAsset:matchingAsset];
265
+ if (bundlePath == nil) {
266
+ dispatch_async(self->_launcherQueue, ^{
267
+ completion(NO, [NSError errorWithDomain:EXUpdatesAppLauncherErrorDomain code:1013 userInfo:@{NSLocalizedDescriptionKey: @"Asset bundlePath was unexpectedly nil"}]);
268
+ });
269
+ return;
270
+ }
269
271
  NSError *error;
270
272
  BOOL success = [NSFileManager.defaultManager copyItemAtPath:bundlePath toPath:[assetLocalUrl path] error:&error];
271
273
  dispatch_async(self->_launcherQueue, ^{
@@ -275,7 +277,7 @@ static NSString * const EXUpdatesAppLauncherErrorDomain = @"AppLauncher";
275
277
  return;
276
278
  }
277
279
  }
278
-
280
+
279
281
  completion(NO, nil);
280
282
  }
281
283
 
@@ -176,6 +176,9 @@ static NSString * const EXUpdatesAppLoaderErrorDomain = @"EXUpdatesAppLoader";
176
176
  [self downloadAsset:asset];
177
177
  } else {
178
178
  NSError *mergeError;
179
+ // merge fields from existing database entry into our current asset object
180
+ // retaining the original object since it's used in self->_assetsToLoad
181
+ // (this is different from on Android, where we keep the database-sourced object instead)
179
182
  [self->_database mergeAsset:asset withExistingEntry:matchingDbEntry error:&mergeError];
180
183
  if (mergeError) {
181
184
  NSLog(@"Failed to merge asset with existing database entry: %@", mergeError.localizedDescription);
@@ -2,6 +2,7 @@
2
2
 
3
3
  #import <EXUpdates/EXUpdatesFileDownloader.h>
4
4
  #import <EXUpdates/EXUpdatesEmbeddedAppLoader.h>
5
+ #import <EXUpdates/EXUpdatesUtils.h>
5
6
 
6
7
  NS_ASSUME_NONNULL_BEGIN
7
8
 
@@ -101,9 +102,7 @@ static NSString * const EXUpdatesEmbeddedAppLoaderErrorDomain = @"EXUpdatesEmbed
101
102
  });
102
103
  } else {
103
104
  NSAssert(asset.mainBundleFilename, @"embedded asset mainBundleFilename must be nonnull");
104
- NSString *bundlePath = asset.mainBundleDir
105
- ? [[NSBundle mainBundle] pathForResource:asset.mainBundleFilename ofType:asset.type inDirectory:asset.mainBundleDir]
106
- : [[NSBundle mainBundle] pathForResource:asset.mainBundleFilename ofType:asset.type];
105
+ NSString *bundlePath = [EXUpdatesUtils pathForBundledAsset:asset];
107
106
  NSAssert(bundlePath, @"NSBundle must contain the expected assets");
108
107
 
109
108
  if (!bundlePath) {
@@ -275,6 +275,17 @@ static NSString * const EXUpdatesDatabaseServerDefinedHeadersKey = @"serverDefin
275
275
  return nil;
276
276
  }
277
277
 
278
+ // check for duplicate rows representing a single file on disk
279
+ NSString * const update3Sql = @"UPDATE assets SET marked_for_deletion = 0 WHERE relative_path IN (\
280
+ SELECT relative_path\
281
+ FROM assets\
282
+ WHERE marked_for_deletion = 0\
283
+ );";
284
+ if ([self _executeSql:update3Sql withArgs:nil error:error] == nil) {
285
+ sqlite3_exec(_db, "ROLLBACK;", NULL, NULL, NULL);
286
+ return nil;
287
+ }
288
+
278
289
  NSString * const selectSql = @"SELECT * FROM assets WHERE marked_for_deletion = 1;";
279
290
  NSArray<NSDictionary *> *rows = [self _executeSql:selectSql withArgs:nil error:error];
280
291
  if (!rows) {
@@ -53,9 +53,14 @@ EX_REGISTER_SINGLETON_MODULE(EXUpdatesAppDelegate)
53
53
  // we just skip in this case.
54
54
  return NO;
55
55
  }
56
+ UIWindow *window = application.delegate.window;
57
+ if ([window.rootViewController.view isKindOfClass:[RCTRootView class]]) {
58
+ RCTRootView *rootView = (RCTRootView *)window.rootViewController.view;
59
+ [rootView.bridge invalidate];
60
+ }
56
61
  self.launchOptions = launchOptions;
57
62
  controller.delegate = self;
58
- [controller startAndShowLaunchScreen:application.delegate.window];
63
+ [controller startAndShowLaunchScreen:window];
59
64
  return YES;
60
65
  }
61
66
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  #import <React/RCTBridge.h>
4
4
 
5
+ #import <EXUpdates/EXUpdatesAsset.h>
5
6
  #import <EXUpdates/EXUpdatesConfig.h>
6
7
 
7
8
  NS_ASSUME_NONNULL_BEGIN
@@ -14,6 +15,8 @@ NS_ASSUME_NONNULL_BEGIN
14
15
  + (void)sendEventToBridge:(nullable RCTBridge *)bridge withType:(NSString *)eventType body:(NSDictionary *)body;
15
16
  + (BOOL)shouldCheckForUpdateWithConfig:(EXUpdatesConfig *)config;
16
17
  + (NSString *)getRuntimeVersionWithConfig:(EXUpdatesConfig *)config;
18
+ + (NSURL *)urlForBundledAsset:(EXUpdatesAsset *)asset;
19
+ + (NSString *)pathForBundledAsset:(EXUpdatesAsset *)asset;
17
20
 
18
21
  @end
19
22
 
@@ -104,6 +104,20 @@ static NSString * const EXUpdatesUtilsErrorDomain = @"EXUpdatesUtils";
104
104
  return config.runtimeVersion ?: config.sdkVersion ?: @"1";
105
105
  }
106
106
 
107
+ + (NSURL *)urlForBundledAsset:(EXUpdatesAsset *)asset
108
+ {
109
+ return asset.mainBundleDir
110
+ ? [[NSBundle mainBundle] URLForResource:asset.mainBundleFilename withExtension:asset.type subdirectory:asset.mainBundleDir]
111
+ : [[NSBundle mainBundle] URLForResource:asset.mainBundleFilename withExtension:asset.type];
112
+ }
113
+
114
+ + (NSString *)pathForBundledAsset:(EXUpdatesAsset *)asset
115
+ {
116
+ return asset.mainBundleDir
117
+ ? [[NSBundle mainBundle] pathForResource:asset.mainBundleFilename ofType:asset.type inDirectory:asset.mainBundleDir]
118
+ : [[NSBundle mainBundle] pathForResource:asset.mainBundleFilename ofType:asset.type];
119
+ }
120
+
107
121
  @end
108
122
 
109
123
  NS_ASSUME_NONNULL_END
@@ -2,6 +2,8 @@
2
2
 
3
3
  #import <XCTest/XCTest.h>
4
4
 
5
+ #import <EXManifests/EXManifestsNewManifest.h>
6
+ #import <EXUpdates/EXUpdatesAsset.h>
5
7
  #import <EXUpdates/EXUpdatesConfig.h>
6
8
  #import <EXUpdates/EXUpdatesDatabase+Tests.h>
7
9
  #import <EXUpdates/EXUpdatesNewUpdate.h>
@@ -172,4 +174,85 @@
172
174
  XCTAssertEqualObjects(expected, actual);
173
175
  }
174
176
 
177
+ - (void)testDeleteUnusedAssets_DuplicateFilenames
178
+ {
179
+ EXManifestsNewManifest *manifest1 = [[EXManifestsNewManifest alloc] initWithRawManifestJSON:@{
180
+ @"runtimeVersion": @"1",
181
+ @"id": @"0eef8214-4833-4089-9dff-b4138a14f196",
182
+ @"createdAt": @"2020-11-11T00:17:54.797Z",
183
+ @"launchAsset": @{@"url": @"https://url.to/bundle1.js", @"contentType": @"application/javascript"}
184
+ }];
185
+ EXManifestsNewManifest *manifest2 = [[EXManifestsNewManifest alloc] initWithRawManifestJSON:@{
186
+ @"runtimeVersion": @"1",
187
+ @"id": @"0eef8214-4833-4089-9dff-b4138a14f197",
188
+ @"createdAt": @"2020-11-11T00:17:55.797Z",
189
+ @"launchAsset": @{@"url": @"https://url.to/bundle2.js", @"contentType": @"application/javascript"}
190
+ }];
191
+
192
+ EXUpdatesAsset *asset1 = [self createMockAssetWithKey:@"key1"];
193
+ EXUpdatesAsset *asset2 = [self createMockAssetWithKey:@"key2"];
194
+ EXUpdatesAsset *asset3 = [self createMockAssetWithKey:@"key3"];
195
+
196
+ // simulate two assets with different keys that share a file on disk
197
+ // this can happen if we, for example, change the format of asset keys that we serve
198
+ asset2.filename = @"same-filename.png";
199
+ asset3.filename = @"same-filename.png";
200
+
201
+ EXUpdatesUpdate *update1 = [EXUpdatesNewUpdate updateWithNewManifest:manifest1 response:nil config:_config database:_db];
202
+ EXUpdatesUpdate *update2 = [EXUpdatesNewUpdate updateWithNewManifest:manifest2 response:nil config:_config database:_db];
203
+
204
+ dispatch_sync(_db.databaseQueue, ^{
205
+ NSError *update1Error;
206
+ [_db addUpdate:update1 error:&update1Error];
207
+ NSError *update2Error;
208
+ [_db addUpdate:update2 error:&update2Error];
209
+ NSError *updateAsset1Error;
210
+ [_db addNewAssets:@[asset1, asset2] toUpdateWithId:update1.updateId error:&updateAsset1Error];
211
+ NSError *updateAsset2Error;
212
+ [_db addNewAssets:@[asset3] toUpdateWithId:update2.updateId error:&updateAsset2Error];
213
+ if (update1Error || update2Error || updateAsset1Error || updateAsset2Error) {
214
+ XCTFail(@"%@ %@ %@ %@", update1Error.localizedDescription, update2Error.localizedDescription, updateAsset1Error.localizedDescription, updateAsset2Error.localizedDescription);
215
+ return;
216
+ }
217
+
218
+ // simulate update1 being reaped, update2 being kept
219
+ NSError *deleteUpdateError;
220
+ [_db deleteUpdates:@[update1] error:&deleteUpdateError];
221
+ if (deleteUpdateError) {
222
+ XCTFail(@"%@", deleteUpdateError.localizedDescription);
223
+ return;
224
+ }
225
+
226
+ NSArray<EXUpdatesAsset *> *assets = [_db allAssetsWithError:nil];
227
+ XCTAssertEqual(3, assets.count); // two bundles and asset1 and asset2
228
+
229
+ NSError *deleteAssetsError;
230
+ NSArray<EXUpdatesAsset *> *deletedAssets = [_db deleteUnusedAssetsWithError:&deleteAssetsError];
231
+ if (deleteAssetsError) {
232
+ XCTFail(@"%@", deleteAssetsError.localizedDescription);
233
+ return;
234
+ }
235
+
236
+ // asset1 should have been deleted, but asset2 should have been kept
237
+ // since it shared a filename with asset3, which is still in use
238
+ XCTAssertEqual(1, deletedAssets.count);
239
+ for (EXUpdatesAsset *deletedAsset in deletedAssets) {
240
+ XCTAssertEqualObjects(@"key1", deletedAsset.key);
241
+ }
242
+
243
+ XCTAssertNil([_db assetWithKey:@"key1" error:nil]);
244
+ XCTAssertNotNil([_db assetWithKey:@"key2" error:nil]);
245
+ XCTAssertNotNil([_db assetWithKey:@"key3" error:nil]);
246
+ });
247
+ }
248
+
249
+ - (EXUpdatesAsset *)createMockAssetWithKey:(NSString *)key
250
+ {
251
+ EXUpdatesAsset *asset = [[EXUpdatesAsset alloc] initWithKey:key type:@"png"];
252
+ asset.downloadTime = [NSDate date];
253
+ asset.contentHash = key;
254
+ asset.filename = [NSString stringWithFormat:@"%@.png", key];
255
+ return asset;
256
+ }
257
+
175
258
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-updates",
3
- "version": "0.10.10",
3
+ "version": "0.10.14",
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",
@@ -50,5 +50,5 @@
50
50
  "fs-extra": "^9.1.0",
51
51
  "memfs": "^3.2.0"
52
52
  },
53
- "gitHead": "712122d3d4079e4d419d3a5fedf39cc011051ab6"
53
+ "gitHead": "8a45d6d743b91f608f0548d2ab02a3cef7434dcf"
54
54
  }