@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 +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +211 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +211 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/data/apps/bundles/remote.ts +17 -0
- package/src/data/apps/bundles/repository.ts +14 -0
- package/src/data/apps/bundles/types.ts +15 -0
- package/src/studio/hooks/useBundleManager.ts +273 -22
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
|
|
1004
|
+
function legacyBundleFileUri(key) {
|
|
989
1005
|
const dir = bundlesCacheDir();
|
|
990
1006
|
return `${dir}${safeName(key)}.jsbundle`;
|
|
991
1007
|
}
|
|
992
|
-
function
|
|
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
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
|
1064
|
-
|
|
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(
|
|
1128
|
-
`${appId}:${commitId ?? "head"}:${platform}:${finalBundle.id}
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|