@comergehq/studio 0.1.13 → 0.1.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/dist/index.mjs CHANGED
@@ -884,6 +884,7 @@ function useThreadMessages(threadId) {
884
884
  import * as React5 from "react";
885
885
  import * as FileSystem from "expo-file-system/legacy";
886
886
  import { Asset } from "expo-asset";
887
+ import { unzip } from "react-native-zip-archive";
887
888
 
888
889
  // src/data/apps/bundles/remote.ts
889
890
  var BundlesRemoteDataSourceImpl = class extends BaseRemote {
@@ -907,6 +908,13 @@ var BundlesRemoteDataSourceImpl = class extends BaseRemote {
907
908
  );
908
909
  return data;
909
910
  }
911
+ async getSignedAssetsDownloadUrl(appId, bundleId, options) {
912
+ const { data } = await api.get(
913
+ `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/assets/download`,
914
+ { params: { redirect: (options == null ? void 0 : options.redirect) ?? false, kind: options == null ? void 0 : options.kind } }
915
+ );
916
+ return data;
917
+ }
910
918
  };
911
919
  var bundlesRemoteDataSource = new BundlesRemoteDataSourceImpl();
912
920
 
@@ -928,6 +936,10 @@ var BundlesRepositoryImpl = class extends BaseRepository {
928
936
  const res = await this.remote.getSignedDownloadUrl(appId, bundleId, options);
929
937
  return this.unwrapOrThrow(res);
930
938
  }
939
+ async getSignedAssetsDownloadUrl(appId, bundleId, options) {
940
+ const res = await this.remote.getSignedAssetsDownloadUrl(appId, bundleId, options);
941
+ return this.unwrapOrThrow(res);
942
+ }
931
943
  };
932
944
  var bundlesRepository = new BundlesRepositoryImpl(bundlesRemoteDataSource);
933
945
 
@@ -979,20 +991,40 @@ async function ensureDir(path) {
979
991
  if (info.exists) return;
980
992
  await FileSystem.makeDirectoryAsync(path, { intermediates: true });
981
993
  }
994
+ async function ensureBundleDir(key) {
995
+ await ensureDir(bundlesCacheDir());
996
+ await ensureDir(bundleDir(key));
997
+ }
982
998
  function baseBundleKey(appId, platform) {
983
999
  return `base:${appId}:${platform}`;
984
1000
  }
985
1001
  function testBundleKey(appId, commitId, platform, bundleId) {
986
1002
  return `test:${appId}:${commitId ?? "head"}:${platform}:${bundleId}`;
987
1003
  }
988
- function toBundleFileUri(key) {
1004
+ function legacyBundleFileUri(key) {
989
1005
  const dir = bundlesCacheDir();
990
1006
  return `${dir}${safeName(key)}.jsbundle`;
991
1007
  }
992
- function toBundleMetaFileUri(key) {
1008
+ function legacyBundleMetaFileUri(key) {
993
1009
  const dir = bundlesCacheDir();
994
1010
  return `${dir}${safeName(key)}.meta.json`;
995
1011
  }
1012
+ function bundleDir(key) {
1013
+ const dir = bundlesCacheDir();
1014
+ return `${dir}${safeName(key)}/`;
1015
+ }
1016
+ function toBundleFileUri(key, platform) {
1017
+ return `${bundleDir(key)}index.${platform}.jsbundle`;
1018
+ }
1019
+ function toBundleMetaFileUri(key) {
1020
+ return `${bundleDir(key)}bundle.meta.json`;
1021
+ }
1022
+ function toAssetsMetaFileUri(key) {
1023
+ return `${bundleDir(key)}assets.meta.json`;
1024
+ }
1025
+ function toAssetsDir(key) {
1026
+ return `${bundleDir(key)}assets/`;
1027
+ }
996
1028
  async function readJsonFile(fileUri) {
997
1029
  try {
998
1030
  const info = await FileSystem.getInfoAsync(fileUri);
@@ -1019,6 +1051,14 @@ async function getExistingNonEmptyFileUri(fileUri) {
1019
1051
  return null;
1020
1052
  }
1021
1053
  }
1054
+ async function getExistingBundleFileUri(key, platform) {
1055
+ const nextPath = toBundleFileUri(key, platform);
1056
+ const next = await getExistingNonEmptyFileUri(nextPath);
1057
+ if (next) return next;
1058
+ const legacyPath = legacyBundleFileUri(key);
1059
+ const legacy = await getExistingNonEmptyFileUri(legacyPath);
1060
+ return legacy;
1061
+ }
1022
1062
  async function downloadIfMissing(url, fileUri) {
1023
1063
  const existing = await getExistingNonEmptyFileUri(fileUri);
1024
1064
  if (existing) return existing;
@@ -1045,9 +1085,12 @@ async function deleteFileIfExists(fileUri) {
1045
1085
  async function hydrateBaseFromEmbeddedAsset(appId, platform, embedded) {
1046
1086
  if (!(embedded == null ? void 0 : embedded.module)) return null;
1047
1087
  const key = baseBundleKey(appId, platform);
1048
- const targetUri = toBundleFileUri(key);
1049
- const existing = await getExistingNonEmptyFileUri(targetUri);
1050
- if (existing) return { bundlePath: existing, meta: embedded.meta ?? null };
1088
+ const existing = await getExistingBundleFileUri(key, platform);
1089
+ if (existing) {
1090
+ return { bundlePath: existing, meta: embedded.meta ?? null };
1091
+ }
1092
+ await ensureBundleDir(key);
1093
+ const targetUri = toBundleFileUri(key, platform);
1051
1094
  const asset = Asset.fromModule(embedded.module);
1052
1095
  await asset.downloadAsync();
1053
1096
  const sourceUri = asset.localUri ?? asset.uri;
@@ -1060,8 +1103,50 @@ async function hydrateBaseFromEmbeddedAsset(appId, platform, embedded) {
1060
1103
  if (!finalUri) return null;
1061
1104
  return { bundlePath: finalUri, meta: embedded.meta ?? null };
1062
1105
  }
1063
- async function safeReplaceFileFromUrl(url, targetUri, tmpKey) {
1064
- const tmpUri = toBundleFileUri(`tmp:${tmpKey}:${Date.now()}`);
1106
+ async function hydrateAssetsFromEmbeddedAsset(appId, platform, key, embedded) {
1107
+ var _a;
1108
+ const moduleId = embedded == null ? void 0 : embedded.assetsModule;
1109
+ if (!moduleId) return false;
1110
+ const assetsMeta = (embedded == null ? void 0 : embedded.assetsMeta) ?? null;
1111
+ const assetsDir = toAssetsDir(key);
1112
+ const metaUri = toAssetsMetaFileUri(key);
1113
+ const existingMeta = await readJsonFile(metaUri);
1114
+ const assetsDirInfo = await FileSystem.getInfoAsync(assetsDir);
1115
+ const assetsDirExists = assetsDirInfo.exists && assetsDirInfo.isDirectory;
1116
+ const checksumMatches = Boolean(existingMeta == null ? void 0 : existingMeta.checksumSha256) && Boolean(assetsMeta == null ? void 0 : assetsMeta.checksumSha256) && (existingMeta == null ? void 0 : existingMeta.checksumSha256) === (assetsMeta == null ? void 0 : assetsMeta.checksumSha256);
1117
+ const embeddedMetaMatches = (_a = existingMeta == null ? void 0 : existingMeta.storageKey) == null ? void 0 : _a.startsWith("embedded:");
1118
+ if (assetsDirExists && checksumMatches && embeddedMetaMatches) {
1119
+ return true;
1120
+ }
1121
+ await ensureBundleDir(key);
1122
+ await ensureDir(assetsDir);
1123
+ const asset = Asset.fromModule(moduleId);
1124
+ await asset.downloadAsync();
1125
+ const sourceUri = asset.localUri ?? asset.uri;
1126
+ if (!sourceUri) return false;
1127
+ const info = await FileSystem.getInfoAsync(sourceUri);
1128
+ if (!info.exists) return false;
1129
+ const zipUri = `${bundleDir(key)}assets.zip`;
1130
+ await deleteFileIfExists(zipUri);
1131
+ await FileSystem.copyAsync({ from: sourceUri, to: zipUri });
1132
+ try {
1133
+ await FileSystem.deleteAsync(assetsDir, { idempotent: true }).catch(() => {
1134
+ });
1135
+ } catch {
1136
+ }
1137
+ await ensureDir(assetsDir);
1138
+ await unzipArchive(zipUri, assetsDir);
1139
+ await writeJsonFile(metaUri, {
1140
+ checksumSha256: (assetsMeta == null ? void 0 : assetsMeta.checksumSha256) ?? null,
1141
+ storageKey: `embedded:${(assetsMeta == null ? void 0 : assetsMeta.checksumSha256) ?? "unknown"}`,
1142
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1143
+ });
1144
+ return true;
1145
+ }
1146
+ async function safeReplaceFileFromUrl(url, targetUri, tmpKey, platform) {
1147
+ const tmpKeySafe = `tmp:${tmpKey}:${Date.now()}`;
1148
+ const tmpUri = toBundleFileUri(tmpKeySafe, platform);
1149
+ await ensureDir(bundleDir(tmpKeySafe));
1065
1150
  try {
1066
1151
  await withRetry(
1067
1152
  async () => {
@@ -1081,6 +1166,82 @@ async function safeReplaceFileFromUrl(url, targetUri, tmpKey) {
1081
1166
  await deleteFileIfExists(tmpUri);
1082
1167
  }
1083
1168
  }
1169
+ async function safeReplaceFileFromUrlToPath(url, targetUri, tmpKey) {
1170
+ const tmpDir = `${bundlesCacheDir()}tmp/`;
1171
+ await ensureDir(tmpDir);
1172
+ const tmpUri = `${tmpDir}${safeName(tmpKey)}.tmp`;
1173
+ try {
1174
+ await withRetry(
1175
+ async () => {
1176
+ await deleteFileIfExists(tmpUri);
1177
+ await FileSystem.downloadAsync(url, tmpUri);
1178
+ const tmpOk = await getExistingNonEmptyFileUri(tmpUri);
1179
+ if (!tmpOk) throw new Error("Downloaded file is empty.");
1180
+ },
1181
+ { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
1182
+ );
1183
+ await deleteFileIfExists(targetUri);
1184
+ await FileSystem.moveAsync({ from: tmpUri, to: targetUri });
1185
+ const finalOk = await getExistingNonEmptyFileUri(targetUri);
1186
+ if (!finalOk) throw new Error("File replacement failed.");
1187
+ return targetUri;
1188
+ } finally {
1189
+ await deleteFileIfExists(tmpUri);
1190
+ }
1191
+ }
1192
+ function getMetroAssets(bundle) {
1193
+ const assets = bundle.assets ?? [];
1194
+ return assets.find((asset) => asset.kind === "metro-assets") ?? null;
1195
+ }
1196
+ async function ensureAssetsForBundle(appId, bundle, key, platform) {
1197
+ var _a;
1198
+ const asset = getMetroAssets(bundle);
1199
+ if (!(asset == null ? void 0 : asset.storageKey)) return;
1200
+ await ensureBundleDir(key);
1201
+ const assetsDir = toAssetsDir(key);
1202
+ await ensureDir(assetsDir);
1203
+ const metaUri = toAssetsMetaFileUri(key);
1204
+ const existingMeta = await readJsonFile(metaUri);
1205
+ const assetsDirInfo = await FileSystem.getInfoAsync(assetsDir);
1206
+ const assetsDirExists = assetsDirInfo.exists && assetsDirInfo.isDirectory;
1207
+ if ((existingMeta == null ? void 0 : existingMeta.checksumSha256) && asset.checksumSha256 && existingMeta.checksumSha256 === asset.checksumSha256 && (existingMeta.storageKey === asset.storageKey || ((_a = existingMeta.storageKey) == null ? void 0 : _a.startsWith("embedded:"))) && assetsDirExists) {
1208
+ return;
1209
+ }
1210
+ const signed = await withRetry(
1211
+ async () => {
1212
+ return await bundlesRepository.getSignedAssetsDownloadUrl(appId, bundle.id, {
1213
+ redirect: false,
1214
+ kind: asset.kind
1215
+ });
1216
+ },
1217
+ { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
1218
+ );
1219
+ const zipUri = `${bundleDir(key)}assets.zip`;
1220
+ await safeReplaceFileFromUrlToPath(signed.url, zipUri, `${appId}:${bundle.id}:${platform}:${asset.kind}`);
1221
+ try {
1222
+ await FileSystem.deleteAsync(assetsDir, { idempotent: true }).catch(() => {
1223
+ });
1224
+ } catch {
1225
+ }
1226
+ await ensureDir(assetsDir);
1227
+ await unzipArchive(zipUri, assetsDir);
1228
+ await writeJsonFile(metaUri, {
1229
+ checksumSha256: asset.checksumSha256 ?? null,
1230
+ storageKey: asset.storageKey,
1231
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1232
+ });
1233
+ }
1234
+ async function unzipArchive(sourceUri, destDir) {
1235
+ try {
1236
+ await unzip(sourceUri, destDir);
1237
+ } catch (e) {
1238
+ throw new Error(
1239
+ `Failed to extract assets archive. Ensure 'react-native-zip-archive' is installed in the host app. ${String(
1240
+ (e == null ? void 0 : e.message) ?? e
1241
+ )}`
1242
+ );
1243
+ }
1244
+ }
1084
1245
  async function pollBundle(appId, bundleId, opts) {
1085
1246
  const start = Date.now();
1086
1247
  while (true) {
@@ -1116,18 +1277,33 @@ async function resolveBundlePath(src, platform, mode) {
1116
1277
  if (finalBundle.status === "failed") {
1117
1278
  throw new Error("Bundle build failed.");
1118
1279
  }
1280
+ let bundleWithAssets = finalBundle;
1281
+ if (finalBundle.status === "succeeded" && (!finalBundle.assets || finalBundle.assets.length === 0)) {
1282
+ try {
1283
+ bundleWithAssets = await bundlesRepository.getById(appId, finalBundle.id);
1284
+ } catch {
1285
+ bundleWithAssets = finalBundle;
1286
+ }
1287
+ }
1119
1288
  const signed = await withRetry(
1120
1289
  async () => {
1121
1290
  return await bundlesRepository.getSignedDownloadUrl(appId, finalBundle.id, { redirect: false });
1122
1291
  },
1123
1292
  { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
1124
1293
  );
1294
+ const key = mode === "base" ? baseBundleKey(appId, platform) : testBundleKey(appId, commitId, platform, finalBundle.id);
1295
+ await ensureBundleDir(key);
1125
1296
  const bundlePath = mode === "base" ? await safeReplaceFileFromUrl(
1126
1297
  signed.url,
1127
- toBundleFileUri(baseBundleKey(appId, platform)),
1128
- `${appId}:${commitId ?? "head"}:${platform}:${finalBundle.id}`
1129
- ) : await downloadIfMissing(signed.url, toBundleFileUri(testBundleKey(appId, commitId, platform, finalBundle.id)));
1130
- return { bundlePath, label: "Ready", bundle: finalBundle };
1298
+ toBundleFileUri(key, platform),
1299
+ `${appId}:${commitId ?? "head"}:${platform}:${finalBundle.id}`,
1300
+ platform
1301
+ ) : await downloadIfMissing(signed.url, toBundleFileUri(key, platform));
1302
+ try {
1303
+ await ensureAssetsForBundle(appId, bundleWithAssets, key, platform);
1304
+ } catch {
1305
+ }
1306
+ return { bundlePath, label: "Ready", bundle: bundleWithAssets };
1131
1307
  }
1132
1308
  function useBundleManager({
1133
1309
  base,
@@ -1168,13 +1344,12 @@ function useBundleManager({
1168
1344
  const hasCompletedFirstNetworkBaseLoadRef = React5.useRef(false);
1169
1345
  const hydrateBaseFromDisk = React5.useCallback(
1170
1346
  async (appId, reason) => {
1171
- var _a;
1347
+ var _a, _b, _c;
1172
1348
  try {
1173
1349
  const dir = bundlesCacheDir();
1174
1350
  await ensureDir(dir);
1175
1351
  const key = baseBundleKey(appId, platform);
1176
- const uri = toBundleFileUri(key);
1177
- let existing = await getExistingNonEmptyFileUri(uri);
1352
+ let existing = await getExistingBundleFileUri(key, platform);
1178
1353
  let embeddedMeta = null;
1179
1354
  if (!existing) {
1180
1355
  const embedded = (_a = embeddedBaseBundlesRef.current) == null ? void 0 : _a[platform];
@@ -1183,14 +1358,25 @@ function useBundleManager({
1183
1358
  existing = hydrated.bundlePath;
1184
1359
  embeddedMeta = hydrated.meta ?? null;
1185
1360
  if (embeddedMeta) {
1361
+ await ensureBundleDir(key);
1186
1362
  await writeJsonFile(toBundleMetaFileUri(key), embeddedMeta);
1363
+ await writeJsonFile(legacyBundleMetaFileUri(key), embeddedMeta);
1187
1364
  }
1188
1365
  }
1189
1366
  }
1190
1367
  if (existing) {
1191
1368
  lastBaseBundlePathRef.current = existing;
1192
1369
  setBundlePath(existing);
1193
- const meta = embeddedMeta ?? await readJsonFile(toBundleMetaFileUri(key));
1370
+ const meta = embeddedMeta ?? await readJsonFile(toBundleMetaFileUri(key)) ?? await readJsonFile(legacyBundleMetaFileUri(key));
1371
+ const embedded = (_b = embeddedBaseBundlesRef.current) == null ? void 0 : _b[platform];
1372
+ const embeddedFingerprint = ((_c = embedded == null ? void 0 : embedded.meta) == null ? void 0 : _c.fingerprint) ?? null;
1373
+ const actualFingerprint = (meta == null ? void 0 : meta.fingerprint) ?? (embeddedMeta == null ? void 0 : embeddedMeta.fingerprint) ?? null;
1374
+ if ((embedded == null ? void 0 : embedded.assetsModule) && embeddedFingerprint && actualFingerprint === embeddedFingerprint) {
1375
+ try {
1376
+ await hydrateAssetsFromEmbeddedAsset(appId, platform, key, embedded);
1377
+ } catch {
1378
+ }
1379
+ }
1194
1380
  if (meta == null ? void 0 : meta.fingerprint) {
1195
1381
  lastBaseFingerprintRef.current = meta.fingerprint;
1196
1382
  }
@@ -1255,7 +1441,16 @@ function useBundleManager({
1255
1441
  lastBaseFingerprintRef.current = fingerprint;
1256
1442
  hasCompletedFirstNetworkBaseLoadRef.current = true;
1257
1443
  initialHydratedBaseFromDiskRef.current = false;
1258
- void writeJsonFile(toBundleMetaFileUri(baseBundleKey(src.appId, platform)), {
1444
+ const metaKey = baseBundleKey(src.appId, platform);
1445
+ await ensureBundleDir(metaKey);
1446
+ void writeJsonFile(toBundleMetaFileUri(metaKey), {
1447
+ fingerprint,
1448
+ bundleId: bundle.id,
1449
+ checksumSha256: bundle.checksumSha256 ?? null,
1450
+ size: bundle.size ?? null,
1451
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1452
+ });
1453
+ void writeJsonFile(legacyBundleMetaFileUri(metaKey), {
1259
1454
  fingerprint,
1260
1455
  bundleId: bundle.id,
1261
1456
  checksumSha256: bundle.checksumSha256 ?? null,