@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.d.mts CHANGED
@@ -6,11 +6,17 @@ import { SupabaseClient } from '@supabase/supabase-js';
6
6
  type EmbeddedBaseBundle = {
7
7
  module: number;
8
8
  meta?: BaseBundleMeta | null;
9
+ assetsModule?: number;
10
+ assetsMeta?: EmbeddedAssetsMeta | null;
9
11
  };
10
12
  type EmbeddedBaseBundles = {
11
13
  ios?: EmbeddedBaseBundle;
12
14
  android?: EmbeddedBaseBundle;
13
15
  };
16
+ type EmbeddedAssetsMeta = {
17
+ checksumSha256: string | null;
18
+ size: number | null;
19
+ };
14
20
  type BaseBundleMeta = {
15
21
  fingerprint: string;
16
22
  bundleId: string;
package/dist/index.d.ts CHANGED
@@ -6,11 +6,17 @@ import { SupabaseClient } from '@supabase/supabase-js';
6
6
  type EmbeddedBaseBundle = {
7
7
  module: number;
8
8
  meta?: BaseBundleMeta | null;
9
+ assetsModule?: number;
10
+ assetsMeta?: EmbeddedAssetsMeta | null;
9
11
  };
10
12
  type EmbeddedBaseBundles = {
11
13
  ios?: EmbeddedBaseBundle;
12
14
  android?: EmbeddedBaseBundle;
13
15
  };
16
+ type EmbeddedAssetsMeta = {
17
+ checksumSha256: string | null;
18
+ size: number | null;
19
+ };
14
20
  type BaseBundleMeta = {
15
21
  fingerprint: string;
16
22
  bundleId: string;
package/dist/index.js CHANGED
@@ -914,6 +914,7 @@ function useThreadMessages(threadId) {
914
914
  var React5 = __toESM(require("react"));
915
915
  var FileSystem = __toESM(require("expo-file-system/legacy"));
916
916
  var import_expo_asset = require("expo-asset");
917
+ var import_react_native_zip_archive = require("react-native-zip-archive");
917
918
 
918
919
  // src/data/apps/bundles/remote.ts
919
920
  var BundlesRemoteDataSourceImpl = class extends BaseRemote {
@@ -937,6 +938,13 @@ var BundlesRemoteDataSourceImpl = class extends BaseRemote {
937
938
  );
938
939
  return data;
939
940
  }
941
+ async getSignedAssetsDownloadUrl(appId, bundleId, options) {
942
+ const { data } = await api.get(
943
+ `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/assets/download`,
944
+ { params: { redirect: (options == null ? void 0 : options.redirect) ?? false, kind: options == null ? void 0 : options.kind } }
945
+ );
946
+ return data;
947
+ }
940
948
  };
941
949
  var bundlesRemoteDataSource = new BundlesRemoteDataSourceImpl();
942
950
 
@@ -958,6 +966,10 @@ var BundlesRepositoryImpl = class extends BaseRepository {
958
966
  const res = await this.remote.getSignedDownloadUrl(appId, bundleId, options);
959
967
  return this.unwrapOrThrow(res);
960
968
  }
969
+ async getSignedAssetsDownloadUrl(appId, bundleId, options) {
970
+ const res = await this.remote.getSignedAssetsDownloadUrl(appId, bundleId, options);
971
+ return this.unwrapOrThrow(res);
972
+ }
961
973
  };
962
974
  var bundlesRepository = new BundlesRepositoryImpl(bundlesRemoteDataSource);
963
975
 
@@ -1009,20 +1021,40 @@ async function ensureDir(path) {
1009
1021
  if (info.exists) return;
1010
1022
  await FileSystem.makeDirectoryAsync(path, { intermediates: true });
1011
1023
  }
1024
+ async function ensureBundleDir(key) {
1025
+ await ensureDir(bundlesCacheDir());
1026
+ await ensureDir(bundleDir(key));
1027
+ }
1012
1028
  function baseBundleKey(appId, platform) {
1013
1029
  return `base:${appId}:${platform}`;
1014
1030
  }
1015
1031
  function testBundleKey(appId, commitId, platform, bundleId) {
1016
1032
  return `test:${appId}:${commitId ?? "head"}:${platform}:${bundleId}`;
1017
1033
  }
1018
- function toBundleFileUri(key) {
1034
+ function legacyBundleFileUri(key) {
1019
1035
  const dir = bundlesCacheDir();
1020
1036
  return `${dir}${safeName(key)}.jsbundle`;
1021
1037
  }
1022
- function toBundleMetaFileUri(key) {
1038
+ function legacyBundleMetaFileUri(key) {
1023
1039
  const dir = bundlesCacheDir();
1024
1040
  return `${dir}${safeName(key)}.meta.json`;
1025
1041
  }
1042
+ function bundleDir(key) {
1043
+ const dir = bundlesCacheDir();
1044
+ return `${dir}${safeName(key)}/`;
1045
+ }
1046
+ function toBundleFileUri(key, platform) {
1047
+ return `${bundleDir(key)}index.${platform}.jsbundle`;
1048
+ }
1049
+ function toBundleMetaFileUri(key) {
1050
+ return `${bundleDir(key)}bundle.meta.json`;
1051
+ }
1052
+ function toAssetsMetaFileUri(key) {
1053
+ return `${bundleDir(key)}assets.meta.json`;
1054
+ }
1055
+ function toAssetsDir(key) {
1056
+ return `${bundleDir(key)}assets/`;
1057
+ }
1026
1058
  async function readJsonFile(fileUri) {
1027
1059
  try {
1028
1060
  const info = await FileSystem.getInfoAsync(fileUri);
@@ -1049,6 +1081,14 @@ async function getExistingNonEmptyFileUri(fileUri) {
1049
1081
  return null;
1050
1082
  }
1051
1083
  }
1084
+ async function getExistingBundleFileUri(key, platform) {
1085
+ const nextPath = toBundleFileUri(key, platform);
1086
+ const next = await getExistingNonEmptyFileUri(nextPath);
1087
+ if (next) return next;
1088
+ const legacyPath = legacyBundleFileUri(key);
1089
+ const legacy = await getExistingNonEmptyFileUri(legacyPath);
1090
+ return legacy;
1091
+ }
1052
1092
  async function downloadIfMissing(url, fileUri) {
1053
1093
  const existing = await getExistingNonEmptyFileUri(fileUri);
1054
1094
  if (existing) return existing;
@@ -1075,9 +1115,12 @@ async function deleteFileIfExists(fileUri) {
1075
1115
  async function hydrateBaseFromEmbeddedAsset(appId, platform, embedded) {
1076
1116
  if (!(embedded == null ? void 0 : embedded.module)) return null;
1077
1117
  const key = baseBundleKey(appId, platform);
1078
- const targetUri = toBundleFileUri(key);
1079
- const existing = await getExistingNonEmptyFileUri(targetUri);
1080
- if (existing) return { bundlePath: existing, meta: embedded.meta ?? null };
1118
+ const existing = await getExistingBundleFileUri(key, platform);
1119
+ if (existing) {
1120
+ return { bundlePath: existing, meta: embedded.meta ?? null };
1121
+ }
1122
+ await ensureBundleDir(key);
1123
+ const targetUri = toBundleFileUri(key, platform);
1081
1124
  const asset = import_expo_asset.Asset.fromModule(embedded.module);
1082
1125
  await asset.downloadAsync();
1083
1126
  const sourceUri = asset.localUri ?? asset.uri;
@@ -1090,8 +1133,50 @@ async function hydrateBaseFromEmbeddedAsset(appId, platform, embedded) {
1090
1133
  if (!finalUri) return null;
1091
1134
  return { bundlePath: finalUri, meta: embedded.meta ?? null };
1092
1135
  }
1093
- async function safeReplaceFileFromUrl(url, targetUri, tmpKey) {
1094
- const tmpUri = toBundleFileUri(`tmp:${tmpKey}:${Date.now()}`);
1136
+ async function hydrateAssetsFromEmbeddedAsset(appId, platform, key, embedded) {
1137
+ var _a;
1138
+ const moduleId = embedded == null ? void 0 : embedded.assetsModule;
1139
+ if (!moduleId) return false;
1140
+ const assetsMeta = (embedded == null ? void 0 : embedded.assetsMeta) ?? null;
1141
+ const assetsDir = toAssetsDir(key);
1142
+ const metaUri = toAssetsMetaFileUri(key);
1143
+ const existingMeta = await readJsonFile(metaUri);
1144
+ const assetsDirInfo = await FileSystem.getInfoAsync(assetsDir);
1145
+ const assetsDirExists = assetsDirInfo.exists && assetsDirInfo.isDirectory;
1146
+ 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);
1147
+ const embeddedMetaMatches = (_a = existingMeta == null ? void 0 : existingMeta.storageKey) == null ? void 0 : _a.startsWith("embedded:");
1148
+ if (assetsDirExists && checksumMatches && embeddedMetaMatches) {
1149
+ return true;
1150
+ }
1151
+ await ensureBundleDir(key);
1152
+ await ensureDir(assetsDir);
1153
+ const asset = import_expo_asset.Asset.fromModule(moduleId);
1154
+ await asset.downloadAsync();
1155
+ const sourceUri = asset.localUri ?? asset.uri;
1156
+ if (!sourceUri) return false;
1157
+ const info = await FileSystem.getInfoAsync(sourceUri);
1158
+ if (!info.exists) return false;
1159
+ const zipUri = `${bundleDir(key)}assets.zip`;
1160
+ await deleteFileIfExists(zipUri);
1161
+ await FileSystem.copyAsync({ from: sourceUri, to: zipUri });
1162
+ try {
1163
+ await FileSystem.deleteAsync(assetsDir, { idempotent: true }).catch(() => {
1164
+ });
1165
+ } catch {
1166
+ }
1167
+ await ensureDir(assetsDir);
1168
+ await unzipArchive(zipUri, assetsDir);
1169
+ await writeJsonFile(metaUri, {
1170
+ checksumSha256: (assetsMeta == null ? void 0 : assetsMeta.checksumSha256) ?? null,
1171
+ storageKey: `embedded:${(assetsMeta == null ? void 0 : assetsMeta.checksumSha256) ?? "unknown"}`,
1172
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1173
+ });
1174
+ return true;
1175
+ }
1176
+ async function safeReplaceFileFromUrl(url, targetUri, tmpKey, platform) {
1177
+ const tmpKeySafe = `tmp:${tmpKey}:${Date.now()}`;
1178
+ const tmpUri = toBundleFileUri(tmpKeySafe, platform);
1179
+ await ensureDir(bundleDir(tmpKeySafe));
1095
1180
  try {
1096
1181
  await withRetry(
1097
1182
  async () => {
@@ -1111,6 +1196,82 @@ async function safeReplaceFileFromUrl(url, targetUri, tmpKey) {
1111
1196
  await deleteFileIfExists(tmpUri);
1112
1197
  }
1113
1198
  }
1199
+ async function safeReplaceFileFromUrlToPath(url, targetUri, tmpKey) {
1200
+ const tmpDir = `${bundlesCacheDir()}tmp/`;
1201
+ await ensureDir(tmpDir);
1202
+ const tmpUri = `${tmpDir}${safeName(tmpKey)}.tmp`;
1203
+ try {
1204
+ await withRetry(
1205
+ async () => {
1206
+ await deleteFileIfExists(tmpUri);
1207
+ await FileSystem.downloadAsync(url, tmpUri);
1208
+ const tmpOk = await getExistingNonEmptyFileUri(tmpUri);
1209
+ if (!tmpOk) throw new Error("Downloaded file is empty.");
1210
+ },
1211
+ { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
1212
+ );
1213
+ await deleteFileIfExists(targetUri);
1214
+ await FileSystem.moveAsync({ from: tmpUri, to: targetUri });
1215
+ const finalOk = await getExistingNonEmptyFileUri(targetUri);
1216
+ if (!finalOk) throw new Error("File replacement failed.");
1217
+ return targetUri;
1218
+ } finally {
1219
+ await deleteFileIfExists(tmpUri);
1220
+ }
1221
+ }
1222
+ function getMetroAssets(bundle) {
1223
+ const assets = bundle.assets ?? [];
1224
+ return assets.find((asset) => asset.kind === "metro-assets") ?? null;
1225
+ }
1226
+ async function ensureAssetsForBundle(appId, bundle, key, platform) {
1227
+ var _a;
1228
+ const asset = getMetroAssets(bundle);
1229
+ if (!(asset == null ? void 0 : asset.storageKey)) return;
1230
+ await ensureBundleDir(key);
1231
+ const assetsDir = toAssetsDir(key);
1232
+ await ensureDir(assetsDir);
1233
+ const metaUri = toAssetsMetaFileUri(key);
1234
+ const existingMeta = await readJsonFile(metaUri);
1235
+ const assetsDirInfo = await FileSystem.getInfoAsync(assetsDir);
1236
+ const assetsDirExists = assetsDirInfo.exists && assetsDirInfo.isDirectory;
1237
+ 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) {
1238
+ return;
1239
+ }
1240
+ const signed = await withRetry(
1241
+ async () => {
1242
+ return await bundlesRepository.getSignedAssetsDownloadUrl(appId, bundle.id, {
1243
+ redirect: false,
1244
+ kind: asset.kind
1245
+ });
1246
+ },
1247
+ { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
1248
+ );
1249
+ const zipUri = `${bundleDir(key)}assets.zip`;
1250
+ await safeReplaceFileFromUrlToPath(signed.url, zipUri, `${appId}:${bundle.id}:${platform}:${asset.kind}`);
1251
+ try {
1252
+ await FileSystem.deleteAsync(assetsDir, { idempotent: true }).catch(() => {
1253
+ });
1254
+ } catch {
1255
+ }
1256
+ await ensureDir(assetsDir);
1257
+ await unzipArchive(zipUri, assetsDir);
1258
+ await writeJsonFile(metaUri, {
1259
+ checksumSha256: asset.checksumSha256 ?? null,
1260
+ storageKey: asset.storageKey,
1261
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1262
+ });
1263
+ }
1264
+ async function unzipArchive(sourceUri, destDir) {
1265
+ try {
1266
+ await (0, import_react_native_zip_archive.unzip)(sourceUri, destDir);
1267
+ } catch (e) {
1268
+ throw new Error(
1269
+ `Failed to extract assets archive. Ensure 'react-native-zip-archive' is installed in the host app. ${String(
1270
+ (e == null ? void 0 : e.message) ?? e
1271
+ )}`
1272
+ );
1273
+ }
1274
+ }
1114
1275
  async function pollBundle(appId, bundleId, opts) {
1115
1276
  const start = Date.now();
1116
1277
  while (true) {
@@ -1146,18 +1307,33 @@ async function resolveBundlePath(src, platform, mode) {
1146
1307
  if (finalBundle.status === "failed") {
1147
1308
  throw new Error("Bundle build failed.");
1148
1309
  }
1310
+ let bundleWithAssets = finalBundle;
1311
+ if (finalBundle.status === "succeeded" && (!finalBundle.assets || finalBundle.assets.length === 0)) {
1312
+ try {
1313
+ bundleWithAssets = await bundlesRepository.getById(appId, finalBundle.id);
1314
+ } catch {
1315
+ bundleWithAssets = finalBundle;
1316
+ }
1317
+ }
1149
1318
  const signed = await withRetry(
1150
1319
  async () => {
1151
1320
  return await bundlesRepository.getSignedDownloadUrl(appId, finalBundle.id, { redirect: false });
1152
1321
  },
1153
1322
  { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
1154
1323
  );
1324
+ const key = mode === "base" ? baseBundleKey(appId, platform) : testBundleKey(appId, commitId, platform, finalBundle.id);
1325
+ await ensureBundleDir(key);
1155
1326
  const bundlePath = mode === "base" ? await safeReplaceFileFromUrl(
1156
1327
  signed.url,
1157
- toBundleFileUri(baseBundleKey(appId, platform)),
1158
- `${appId}:${commitId ?? "head"}:${platform}:${finalBundle.id}`
1159
- ) : await downloadIfMissing(signed.url, toBundleFileUri(testBundleKey(appId, commitId, platform, finalBundle.id)));
1160
- return { bundlePath, label: "Ready", bundle: finalBundle };
1328
+ toBundleFileUri(key, platform),
1329
+ `${appId}:${commitId ?? "head"}:${platform}:${finalBundle.id}`,
1330
+ platform
1331
+ ) : await downloadIfMissing(signed.url, toBundleFileUri(key, platform));
1332
+ try {
1333
+ await ensureAssetsForBundle(appId, bundleWithAssets, key, platform);
1334
+ } catch {
1335
+ }
1336
+ return { bundlePath, label: "Ready", bundle: bundleWithAssets };
1161
1337
  }
1162
1338
  function useBundleManager({
1163
1339
  base,
@@ -1198,13 +1374,12 @@ function useBundleManager({
1198
1374
  const hasCompletedFirstNetworkBaseLoadRef = React5.useRef(false);
1199
1375
  const hydrateBaseFromDisk = React5.useCallback(
1200
1376
  async (appId, reason) => {
1201
- var _a;
1377
+ var _a, _b, _c;
1202
1378
  try {
1203
1379
  const dir = bundlesCacheDir();
1204
1380
  await ensureDir(dir);
1205
1381
  const key = baseBundleKey(appId, platform);
1206
- const uri = toBundleFileUri(key);
1207
- let existing = await getExistingNonEmptyFileUri(uri);
1382
+ let existing = await getExistingBundleFileUri(key, platform);
1208
1383
  let embeddedMeta = null;
1209
1384
  if (!existing) {
1210
1385
  const embedded = (_a = embeddedBaseBundlesRef.current) == null ? void 0 : _a[platform];
@@ -1213,14 +1388,25 @@ function useBundleManager({
1213
1388
  existing = hydrated.bundlePath;
1214
1389
  embeddedMeta = hydrated.meta ?? null;
1215
1390
  if (embeddedMeta) {
1391
+ await ensureBundleDir(key);
1216
1392
  await writeJsonFile(toBundleMetaFileUri(key), embeddedMeta);
1393
+ await writeJsonFile(legacyBundleMetaFileUri(key), embeddedMeta);
1217
1394
  }
1218
1395
  }
1219
1396
  }
1220
1397
  if (existing) {
1221
1398
  lastBaseBundlePathRef.current = existing;
1222
1399
  setBundlePath(existing);
1223
- const meta = embeddedMeta ?? await readJsonFile(toBundleMetaFileUri(key));
1400
+ const meta = embeddedMeta ?? await readJsonFile(toBundleMetaFileUri(key)) ?? await readJsonFile(legacyBundleMetaFileUri(key));
1401
+ const embedded = (_b = embeddedBaseBundlesRef.current) == null ? void 0 : _b[platform];
1402
+ const embeddedFingerprint = ((_c = embedded == null ? void 0 : embedded.meta) == null ? void 0 : _c.fingerprint) ?? null;
1403
+ const actualFingerprint = (meta == null ? void 0 : meta.fingerprint) ?? (embeddedMeta == null ? void 0 : embeddedMeta.fingerprint) ?? null;
1404
+ if ((embedded == null ? void 0 : embedded.assetsModule) && embeddedFingerprint && actualFingerprint === embeddedFingerprint) {
1405
+ try {
1406
+ await hydrateAssetsFromEmbeddedAsset(appId, platform, key, embedded);
1407
+ } catch {
1408
+ }
1409
+ }
1224
1410
  if (meta == null ? void 0 : meta.fingerprint) {
1225
1411
  lastBaseFingerprintRef.current = meta.fingerprint;
1226
1412
  }
@@ -1285,7 +1471,16 @@ function useBundleManager({
1285
1471
  lastBaseFingerprintRef.current = fingerprint;
1286
1472
  hasCompletedFirstNetworkBaseLoadRef.current = true;
1287
1473
  initialHydratedBaseFromDiskRef.current = false;
1288
- void writeJsonFile(toBundleMetaFileUri(baseBundleKey(src.appId, platform)), {
1474
+ const metaKey = baseBundleKey(src.appId, platform);
1475
+ await ensureBundleDir(metaKey);
1476
+ void writeJsonFile(toBundleMetaFileUri(metaKey), {
1477
+ fingerprint,
1478
+ bundleId: bundle.id,
1479
+ checksumSha256: bundle.checksumSha256 ?? null,
1480
+ size: bundle.size ?? null,
1481
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1482
+ });
1483
+ void writeJsonFile(legacyBundleMetaFileUri(metaKey), {
1289
1484
  fingerprint,
1290
1485
  bundleId: bundle.id,
1291
1486
  checksumSha256: bundle.checksumSha256 ?? null,