@gallop.software/studio 2.1.5 → 2.1.7

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.
@@ -11,7 +11,7 @@
11
11
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
12
12
  }
13
13
  </style>
14
- <script type="module" crossorigin src="/assets/index-CUv_Vpva.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-Cu_gvU_L.js"></script>
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -891,9 +891,35 @@ async function handleFolderImages(request) {
891
891
  }
892
892
 
893
893
  // src/handlers/files.ts
894
+ import { promises as fs6 } from "fs";
895
+ import path6 from "path";
896
+ import sharp2 from "sharp";
897
+
898
+ // src/handlers/utils/folders.ts
894
899
  import { promises as fs5 } from "fs";
895
900
  import path5 from "path";
896
- import sharp2 from "sharp";
901
+ async function deleteEmptyFolders(folderPath) {
902
+ const publicPath = getPublicPath();
903
+ const normalizedFolder = path5.resolve(folderPath);
904
+ const normalizedPublic = path5.resolve(publicPath);
905
+ if (normalizedFolder === normalizedPublic) {
906
+ return;
907
+ }
908
+ if (!normalizedFolder.startsWith(normalizedPublic)) {
909
+ return;
910
+ }
911
+ try {
912
+ const entries = await fs5.readdir(folderPath);
913
+ if (entries.length === 0) {
914
+ await fs5.rmdir(folderPath);
915
+ const parentFolder = path5.dirname(folderPath);
916
+ await deleteEmptyFolders(parentFolder);
917
+ }
918
+ } catch {
919
+ }
920
+ }
921
+
922
+ // src/handlers/files.ts
897
923
  async function handleUpload(request) {
898
924
  try {
899
925
  const formData = await request.formData();
@@ -905,7 +931,7 @@ async function handleUpload(request) {
905
931
  const bytes = await file.arrayBuffer();
906
932
  const buffer = Buffer.from(bytes);
907
933
  const fileName = file.name;
908
- const ext = path5.extname(fileName).toLowerCase();
934
+ const ext = path6.extname(fileName).toLowerCase();
909
935
  const isImage = isImageFile(fileName);
910
936
  const isMedia = isMediaFile(fileName);
911
937
  const meta = await loadMeta();
@@ -923,7 +949,7 @@ async function handleUpload(request) {
923
949
  }
924
950
  let imageKey = "/" + (relativeDir ? `${relativeDir}/${fileName}` : fileName);
925
951
  if (meta[imageKey]) {
926
- const baseName = path5.basename(fileName, ext);
952
+ const baseName = path6.basename(fileName, ext);
927
953
  let counter = 1;
928
954
  let newFileName = `${baseName}-${counter}${ext}`;
929
955
  let newKey = "/" + (relativeDir ? `${relativeDir}/${newFileName}` : newFileName);
@@ -934,10 +960,10 @@ async function handleUpload(request) {
934
960
  }
935
961
  imageKey = newKey;
936
962
  }
937
- const actualFileName = path5.basename(imageKey);
963
+ const actualFileName = path6.basename(imageKey);
938
964
  const uploadDir = getPublicPath(relativeDir);
939
- await fs5.mkdir(uploadDir, { recursive: true });
940
- await fs5.writeFile(path5.join(uploadDir, actualFileName), buffer);
965
+ await fs6.mkdir(uploadDir, { recursive: true });
966
+ await fs6.writeFile(path6.join(uploadDir, actualFileName), buffer);
941
967
  if (!isMedia) {
942
968
  return jsonResponse({
943
969
  success: true,
@@ -978,6 +1004,7 @@ async function handleDelete(request) {
978
1004
  const meta = await loadMeta();
979
1005
  const deleted = [];
980
1006
  const errors = [];
1007
+ const sourceFolders = /* @__PURE__ */ new Set();
981
1008
  for (const itemPath of paths) {
982
1009
  try {
983
1010
  if (!itemPath.startsWith("public/")) {
@@ -986,12 +1013,13 @@ async function handleDelete(request) {
986
1013
  }
987
1014
  const absolutePath = getWorkspacePath(itemPath);
988
1015
  const imageKey = "/" + itemPath.replace(/^public\//, "");
1016
+ sourceFolders.add(path6.dirname(absolutePath));
989
1017
  const entry = meta[imageKey];
990
1018
  const isPushedToCloud = entry?.c !== void 0;
991
1019
  try {
992
- const stats = await fs5.stat(absolutePath);
1020
+ const stats = await fs6.stat(absolutePath);
993
1021
  if (stats.isDirectory()) {
994
- await fs5.rm(absolutePath, { recursive: true });
1022
+ await fs6.rm(absolutePath, { recursive: true });
995
1023
  const prefix = imageKey + "/";
996
1024
  for (const key of Object.keys(meta)) {
997
1025
  if (key.startsWith(prefix) || key === imageKey) {
@@ -1000,7 +1028,7 @@ async function handleDelete(request) {
1000
1028
  for (const thumbPath of getAllThumbnailPaths(key)) {
1001
1029
  const absoluteThumbPath = getPublicPath(thumbPath);
1002
1030
  try {
1003
- await fs5.unlink(absoluteThumbPath);
1031
+ await fs6.unlink(absoluteThumbPath);
1004
1032
  } catch {
1005
1033
  }
1006
1034
  }
@@ -1009,14 +1037,14 @@ async function handleDelete(request) {
1009
1037
  }
1010
1038
  }
1011
1039
  } else {
1012
- await fs5.unlink(absolutePath);
1040
+ await fs6.unlink(absolutePath);
1013
1041
  const isInImagesFolder = itemPath.startsWith("public/images/");
1014
1042
  if (!isInImagesFolder && entry) {
1015
1043
  if (!isPushedToCloud) {
1016
1044
  for (const thumbPath of getAllThumbnailPaths(imageKey)) {
1017
1045
  const absoluteThumbPath = getPublicPath(thumbPath);
1018
1046
  try {
1019
- await fs5.unlink(absoluteThumbPath);
1047
+ await fs6.unlink(absoluteThumbPath);
1020
1048
  } catch {
1021
1049
  }
1022
1050
  }
@@ -1049,6 +1077,9 @@ async function handleDelete(request) {
1049
1077
  }
1050
1078
  }
1051
1079
  await saveMeta(meta);
1080
+ for (const folder of sourceFolders) {
1081
+ await deleteEmptyFolders(folder);
1082
+ }
1052
1083
  return jsonResponse({
1053
1084
  success: true,
1054
1085
  deleted,
@@ -1075,12 +1106,12 @@ async function handleCreateFolder(request) {
1075
1106
  return jsonResponse({ error: "Invalid path" }, { status: 400 });
1076
1107
  }
1077
1108
  try {
1078
- await fs5.access(folderPath);
1109
+ await fs6.access(folderPath);
1079
1110
  return jsonResponse({ error: "A folder with this name already exists" }, { status: 400 });
1080
1111
  } catch {
1081
1112
  }
1082
- await fs5.mkdir(folderPath, { recursive: true });
1083
- return jsonResponse({ success: true, path: path5.join(safePath, sanitizedName) });
1113
+ await fs6.mkdir(folderPath, { recursive: true });
1114
+ return jsonResponse({ success: true, path: path6.join(safePath, sanitizedName) });
1084
1115
  } catch (error) {
1085
1116
  console.error("Failed to create folder:", error);
1086
1117
  return jsonResponse({ error: "Failed to create folder" }, { status: 500 });
@@ -1098,29 +1129,29 @@ async function handleRename(request) {
1098
1129
  }
1099
1130
  const safePath = oldPath.replace(/\.\./g, "");
1100
1131
  const absoluteOldPath = getWorkspacePath(safePath);
1101
- const parentDir = path5.dirname(absoluteOldPath);
1102
- const absoluteNewPath = path5.join(parentDir, sanitizedName);
1132
+ const parentDir = path6.dirname(absoluteOldPath);
1133
+ const absoluteNewPath = path6.join(parentDir, sanitizedName);
1103
1134
  if (!absoluteOldPath.startsWith(getPublicPath())) {
1104
1135
  return jsonResponse({ error: "Invalid path" }, { status: 400 });
1105
1136
  }
1106
1137
  try {
1107
- await fs5.access(absoluteOldPath);
1138
+ await fs6.access(absoluteOldPath);
1108
1139
  } catch {
1109
1140
  return jsonResponse({ error: "File or folder not found" }, { status: 404 });
1110
1141
  }
1111
1142
  try {
1112
- await fs5.access(absoluteNewPath);
1143
+ await fs6.access(absoluteNewPath);
1113
1144
  return jsonResponse({ error: "An item with this name already exists" }, { status: 400 });
1114
1145
  } catch {
1115
1146
  }
1116
- const stats = await fs5.stat(absoluteOldPath);
1147
+ const stats = await fs6.stat(absoluteOldPath);
1117
1148
  const isFile = stats.isFile();
1118
- const isImage = isFile && isImageFile(path5.basename(oldPath));
1119
- await fs5.rename(absoluteOldPath, absoluteNewPath);
1149
+ const isImage = isFile && isImageFile(path6.basename(oldPath));
1150
+ await fs6.rename(absoluteOldPath, absoluteNewPath);
1120
1151
  if (isImage) {
1121
1152
  const meta = await loadMeta();
1122
1153
  const oldRelativePath = safePath.replace(/^public\//, "");
1123
- const newRelativePath = path5.join(path5.dirname(oldRelativePath), sanitizedName);
1154
+ const newRelativePath = path6.join(path6.dirname(oldRelativePath), sanitizedName);
1124
1155
  const oldKey = "/" + oldRelativePath;
1125
1156
  const newKey = "/" + newRelativePath;
1126
1157
  if (meta[oldKey]) {
@@ -1130,9 +1161,9 @@ async function handleRename(request) {
1130
1161
  for (let i = 0; i < oldThumbPaths.length; i++) {
1131
1162
  const oldThumbPath = getPublicPath(oldThumbPaths[i]);
1132
1163
  const newThumbPath = getPublicPath(newThumbPaths[i]);
1133
- await fs5.mkdir(path5.dirname(newThumbPath), { recursive: true });
1164
+ await fs6.mkdir(path6.dirname(newThumbPath), { recursive: true });
1134
1165
  try {
1135
- await fs5.rename(oldThumbPath, newThumbPath);
1166
+ await fs6.rename(oldThumbPath, newThumbPath);
1136
1167
  } catch {
1137
1168
  }
1138
1169
  }
@@ -1141,7 +1172,7 @@ async function handleRename(request) {
1141
1172
  }
1142
1173
  await saveMeta(meta);
1143
1174
  }
1144
- const newPath = path5.join(path5.dirname(safePath), sanitizedName);
1175
+ const newPath = path6.join(path6.dirname(safePath), sanitizedName);
1145
1176
  return jsonResponse({ success: true, newPath });
1146
1177
  } catch (error) {
1147
1178
  console.error("Failed to rename:", error);
@@ -1176,21 +1207,22 @@ async function handleMoveStream(request) {
1176
1207
  controller.close();
1177
1208
  return;
1178
1209
  }
1179
- await fs5.mkdir(absoluteDestination, { recursive: true });
1210
+ await fs6.mkdir(absoluteDestination, { recursive: true });
1180
1211
  const meta = await loadMeta();
1181
1212
  const cdnUrls = getCdnUrls(meta);
1182
1213
  const r2PublicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "") || "";
1183
1214
  const moved = [];
1184
1215
  const errors = [];
1216
+ const sourceFolders = /* @__PURE__ */ new Set();
1185
1217
  const total = paths.length;
1186
1218
  sendEvent({ type: "start", total });
1187
1219
  for (let i = 0; i < paths.length; i++) {
1188
1220
  const itemPath = paths[i];
1189
1221
  const safePath = itemPath.replace(/\.\./g, "");
1190
- const itemName = path5.basename(safePath);
1191
- const newAbsolutePath = path5.join(absoluteDestination, itemName);
1222
+ const itemName = path6.basename(safePath);
1223
+ const newAbsolutePath = path6.join(absoluteDestination, itemName);
1192
1224
  const oldRelativePath = safePath.replace(/^public\//, "");
1193
- const newRelativePath = path5.join(safeDestination.replace(/^public\//, ""), itemName);
1225
+ const newRelativePath = path6.join(safeDestination.replace(/^public\//, ""), itemName);
1194
1226
  const oldKey = "/" + oldRelativePath;
1195
1227
  const newKey = "/" + newRelativePath;
1196
1228
  sendEvent({
@@ -1212,11 +1244,13 @@ async function handleMoveStream(request) {
1212
1244
  const isPushedToR2 = isInCloud && r2PublicUrl && fileCdnUrl === r2PublicUrl;
1213
1245
  const hasProcessedThumbnails = isProcessed(entry);
1214
1246
  try {
1247
+ const sourceFolder = path6.dirname(getWorkspacePath(safePath));
1248
+ sourceFolders.add(sourceFolder);
1215
1249
  if (isRemote && isImage) {
1216
1250
  const remoteUrl = `${fileCdnUrl}${oldKey}`;
1217
1251
  const buffer = await downloadFromRemoteUrl(remoteUrl);
1218
- await fs5.mkdir(path5.dirname(newAbsolutePath), { recursive: true });
1219
- await fs5.writeFile(newAbsolutePath, buffer);
1252
+ await fs6.mkdir(path6.dirname(newAbsolutePath), { recursive: true });
1253
+ await fs6.writeFile(newAbsolutePath, buffer);
1220
1254
  const newEntry = {
1221
1255
  o: entry?.o,
1222
1256
  b: entry?.b
@@ -1226,8 +1260,8 @@ async function handleMoveStream(request) {
1226
1260
  moved.push(itemPath);
1227
1261
  } else if (isPushedToR2 && isImage) {
1228
1262
  const buffer = await downloadFromCdn(oldKey);
1229
- await fs5.mkdir(path5.dirname(newAbsolutePath), { recursive: true });
1230
- await fs5.writeFile(newAbsolutePath, buffer);
1263
+ await fs6.mkdir(path6.dirname(newAbsolutePath), { recursive: true });
1264
+ await fs6.writeFile(newAbsolutePath, buffer);
1231
1265
  let newEntry = {
1232
1266
  o: entry?.o,
1233
1267
  b: entry?.b
@@ -1242,7 +1276,7 @@ async function handleMoveStream(request) {
1242
1276
  }
1243
1277
  await deleteFromCdn(oldKey, hasProcessedThumbnails);
1244
1278
  try {
1245
- await fs5.unlink(newAbsolutePath);
1279
+ await fs6.unlink(newAbsolutePath);
1246
1280
  } catch {
1247
1281
  }
1248
1282
  if (hasProcessedThumbnails) {
@@ -1254,33 +1288,34 @@ async function handleMoveStream(request) {
1254
1288
  moved.push(itemPath);
1255
1289
  } else {
1256
1290
  const absolutePath = getWorkspacePath(safePath);
1257
- if (absoluteDestination.startsWith(absolutePath + path5.sep)) {
1291
+ if (absoluteDestination.startsWith(absolutePath + path6.sep)) {
1258
1292
  errors.push(`Cannot move ${itemName} into itself`);
1259
1293
  continue;
1260
1294
  }
1261
1295
  try {
1262
- await fs5.access(absolutePath);
1296
+ await fs6.access(absolutePath);
1263
1297
  } catch {
1264
1298
  errors.push(`${itemName} not found`);
1265
1299
  continue;
1266
1300
  }
1267
1301
  try {
1268
- await fs5.access(newAbsolutePath);
1302
+ await fs6.access(newAbsolutePath);
1269
1303
  errors.push(`${itemName} already exists in destination`);
1270
1304
  continue;
1271
1305
  } catch {
1272
1306
  }
1273
- await fs5.rename(absolutePath, newAbsolutePath);
1274
- const stats = await fs5.stat(newAbsolutePath);
1307
+ await fs6.rename(absolutePath, newAbsolutePath);
1308
+ const stats = await fs6.stat(newAbsolutePath);
1275
1309
  if (stats.isFile() && isImage && entry) {
1276
1310
  const oldThumbPaths = getAllThumbnailPaths(oldKey);
1277
1311
  const newThumbPaths = getAllThumbnailPaths(newKey);
1278
1312
  for (let j = 0; j < oldThumbPaths.length; j++) {
1279
1313
  const oldThumbPath = getPublicPath(oldThumbPaths[j]);
1280
1314
  const newThumbPath = getPublicPath(newThumbPaths[j]);
1281
- await fs5.mkdir(path5.dirname(newThumbPath), { recursive: true });
1315
+ sourceFolders.add(path6.dirname(oldThumbPath));
1316
+ await fs6.mkdir(path6.dirname(newThumbPath), { recursive: true });
1282
1317
  try {
1283
- await fs5.rename(oldThumbPath, newThumbPath);
1318
+ await fs6.rename(oldThumbPath, newThumbPath);
1284
1319
  } catch {
1285
1320
  }
1286
1321
  }
@@ -1305,6 +1340,9 @@ async function handleMoveStream(request) {
1305
1340
  }
1306
1341
  }
1307
1342
  await saveMeta(meta);
1343
+ for (const folder of sourceFolders) {
1344
+ await deleteEmptyFolders(folder);
1345
+ }
1308
1346
  sendEvent({
1309
1347
  type: "complete",
1310
1348
  moved: moved.length,
@@ -1329,8 +1367,8 @@ async function handleMoveStream(request) {
1329
1367
  }
1330
1368
 
1331
1369
  // src/handlers/images.ts
1332
- import { promises as fs6 } from "fs";
1333
- import path6 from "path";
1370
+ import { promises as fs7 } from "fs";
1371
+ import path7 from "path";
1334
1372
  import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2 } from "@aws-sdk/client-s3";
1335
1373
  async function handleSync(request) {
1336
1374
  const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
@@ -1360,6 +1398,7 @@ async function handleSync(request) {
1360
1398
  const pushed = [];
1361
1399
  const errors = [];
1362
1400
  const urlsToPurge = [];
1401
+ const sourceFolders = /* @__PURE__ */ new Set();
1363
1402
  for (let imageKey of imageKeys) {
1364
1403
  if (!imageKey.startsWith("/")) {
1365
1404
  imageKey = `/${imageKey}`;
@@ -1384,7 +1423,7 @@ async function handleSync(request) {
1384
1423
  } else {
1385
1424
  const originalLocalPath = getPublicPath(imageKey);
1386
1425
  try {
1387
- originalBuffer = await fs6.readFile(originalLocalPath);
1426
+ originalBuffer = await fs7.readFile(originalLocalPath);
1388
1427
  } catch {
1389
1428
  errors.push(`Original file not found: ${imageKey}`);
1390
1429
  continue;
@@ -1403,7 +1442,7 @@ async function handleSync(request) {
1403
1442
  for (const thumbPath of getAllThumbnailPaths(imageKey)) {
1404
1443
  const localPath = getPublicPath(thumbPath);
1405
1444
  try {
1406
- const fileBuffer = await fs6.readFile(localPath);
1445
+ const fileBuffer = await fs7.readFile(localPath);
1407
1446
  await r2.send(
1408
1447
  new PutObjectCommand2({
1409
1448
  Bucket: bucketName,
@@ -1420,15 +1459,17 @@ async function handleSync(request) {
1420
1459
  entry.c = cdnIndex;
1421
1460
  if (!isRemote) {
1422
1461
  const originalLocalPath = getPublicPath(imageKey);
1462
+ sourceFolders.add(path7.dirname(originalLocalPath));
1423
1463
  for (const thumbPath of getAllThumbnailPaths(imageKey)) {
1424
1464
  const localPath = getPublicPath(thumbPath);
1465
+ sourceFolders.add(path7.dirname(localPath));
1425
1466
  try {
1426
- await fs6.unlink(localPath);
1467
+ await fs7.unlink(localPath);
1427
1468
  } catch {
1428
1469
  }
1429
1470
  }
1430
1471
  try {
1431
- await fs6.unlink(originalLocalPath);
1472
+ await fs7.unlink(originalLocalPath);
1432
1473
  } catch {
1433
1474
  }
1434
1475
  }
@@ -1439,6 +1480,9 @@ async function handleSync(request) {
1439
1480
  }
1440
1481
  }
1441
1482
  await saveMeta(meta);
1483
+ for (const folder of sourceFolders) {
1484
+ await deleteEmptyFolders(folder);
1485
+ }
1442
1486
  if (urlsToPurge.length > 0) {
1443
1487
  await purgeCloudflareCache(urlsToPurge);
1444
1488
  }
@@ -1610,32 +1654,32 @@ async function handleReprocessStream(request) {
1610
1654
  const isRemote = existingCdnIndex !== void 0 && !isInOurR2;
1611
1655
  const originalPath = getPublicPath(imageKey);
1612
1656
  try {
1613
- buffer = await fs6.readFile(originalPath);
1657
+ buffer = await fs7.readFile(originalPath);
1614
1658
  } catch {
1615
1659
  if (isInOurR2) {
1616
1660
  buffer = await downloadFromCdn(imageKey);
1617
- const dir = path6.dirname(originalPath);
1618
- await fs6.mkdir(dir, { recursive: true });
1619
- await fs6.writeFile(originalPath, buffer);
1661
+ const dir = path7.dirname(originalPath);
1662
+ await fs7.mkdir(dir, { recursive: true });
1663
+ await fs7.writeFile(originalPath, buffer);
1620
1664
  } else if (isRemote && existingCdnUrl) {
1621
1665
  const remoteUrl = `${existingCdnUrl}${imageKey}`;
1622
1666
  buffer = await downloadFromRemoteUrl(remoteUrl);
1623
- const dir = path6.dirname(originalPath);
1624
- await fs6.mkdir(dir, { recursive: true });
1625
- await fs6.writeFile(originalPath, buffer);
1667
+ const dir = path7.dirname(originalPath);
1668
+ await fs7.mkdir(dir, { recursive: true });
1669
+ await fs7.writeFile(originalPath, buffer);
1626
1670
  } else {
1627
1671
  throw new Error(`File not found: ${imageKey}`);
1628
1672
  }
1629
1673
  }
1630
- const ext = path6.extname(imageKey).toLowerCase();
1674
+ const ext = path7.extname(imageKey).toLowerCase();
1631
1675
  const isSvg = ext === ".svg";
1632
1676
  if (isSvg) {
1633
- const imageDir = path6.dirname(imageKey.slice(1));
1677
+ const imageDir = path7.dirname(imageKey.slice(1));
1634
1678
  const imagesPath = getPublicPath("images", imageDir === "." ? "" : imageDir);
1635
- await fs6.mkdir(imagesPath, { recursive: true });
1636
- const fileName = path6.basename(imageKey);
1637
- const destPath = path6.join(imagesPath, fileName);
1638
- await fs6.writeFile(destPath, buffer);
1679
+ await fs7.mkdir(imagesPath, { recursive: true });
1680
+ const fileName = path7.basename(imageKey);
1681
+ const destPath = path7.join(imagesPath, fileName);
1682
+ await fs7.writeFile(destPath, buffer);
1639
1683
  meta[imageKey] = {
1640
1684
  ...entry,
1641
1685
  o: { w: 0, h: 0 },
@@ -1655,7 +1699,7 @@ async function handleReprocessStream(request) {
1655
1699
  }
1656
1700
  await deleteLocalThumbnails(imageKey);
1657
1701
  try {
1658
- await fs6.unlink(originalPath);
1702
+ await fs7.unlink(originalPath);
1659
1703
  } catch {
1660
1704
  }
1661
1705
  }
@@ -1734,8 +1778,8 @@ async function handleDownloadStream(request) {
1734
1778
  try {
1735
1779
  const imageBuffer = await downloadFromCdn(imageKey);
1736
1780
  const localPath = getPublicPath(imageKey.replace(/^\//, ""));
1737
- await fs6.mkdir(path6.dirname(localPath), { recursive: true });
1738
- await fs6.writeFile(localPath, imageBuffer);
1781
+ await fs7.mkdir(path7.dirname(localPath), { recursive: true });
1782
+ await fs7.writeFile(localPath, imageBuffer);
1739
1783
  await deleteThumbnailsFromCdn(imageKey);
1740
1784
  const wasProcessed = isProcessed(entry);
1741
1785
  delete entry.c;
@@ -1797,8 +1841,8 @@ async function handleDownloadStream(request) {
1797
1841
  }
1798
1842
 
1799
1843
  // src/handlers/scan.ts
1800
- import { promises as fs7 } from "fs";
1801
- import path7 from "path";
1844
+ import { promises as fs8 } from "fs";
1845
+ import path8 from "path";
1802
1846
  import sharp3 from "sharp";
1803
1847
  import { encode as encode2 } from "blurhash";
1804
1848
  async function handleScanStream() {
@@ -1821,10 +1865,10 @@ async function handleScanStream() {
1821
1865
  const allFiles = [];
1822
1866
  async function scanDir(dir, relativePath = "") {
1823
1867
  try {
1824
- const entries = await fs7.readdir(dir, { withFileTypes: true });
1868
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
1825
1869
  for (const entry of entries) {
1826
1870
  if (entry.name.startsWith(".")) continue;
1827
- const fullPath = path7.join(dir, entry.name);
1871
+ const fullPath = path8.join(dir, entry.name);
1828
1872
  const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1829
1873
  if (relPath === "images" || relPath.startsWith("images/")) continue;
1830
1874
  if (entry.isDirectory()) {
@@ -1854,7 +1898,7 @@ async function handleScanStream() {
1854
1898
  continue;
1855
1899
  }
1856
1900
  if (meta[imageKey]) {
1857
- const ext = path7.extname(relativePath);
1901
+ const ext = path8.extname(relativePath);
1858
1902
  const baseName = relativePath.slice(0, -ext.length);
1859
1903
  let counter = 1;
1860
1904
  let newKey = `/${baseName}-${counter}${ext}`;
@@ -1865,7 +1909,7 @@ async function handleScanStream() {
1865
1909
  const newRelativePath = `${baseName}-${counter}${ext}`;
1866
1910
  const newFullPath = getPublicPath(newRelativePath);
1867
1911
  try {
1868
- await fs7.rename(fullPath, newFullPath);
1912
+ await fs8.rename(fullPath, newFullPath);
1869
1913
  renamed.push({ from: relativePath, to: newRelativePath });
1870
1914
  relativePath = newRelativePath;
1871
1915
  fullPath = newFullPath;
@@ -1879,12 +1923,12 @@ async function handleScanStream() {
1879
1923
  try {
1880
1924
  const isImage = isImageFile(relativePath);
1881
1925
  if (isImage) {
1882
- const ext = path7.extname(relativePath).toLowerCase();
1926
+ const ext = path8.extname(relativePath).toLowerCase();
1883
1927
  if (ext === ".svg") {
1884
1928
  meta[imageKey] = { o: { w: 0, h: 0 }, b: "" };
1885
1929
  } else {
1886
1930
  try {
1887
- const buffer = await fs7.readFile(fullPath);
1931
+ const buffer = await fs8.readFile(fullPath);
1888
1932
  const metadata = await sharp3(buffer).metadata();
1889
1933
  const { data, info } = await sharp3(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1890
1934
  const blurhash = encode2(new Uint8ClampedArray(data), info.width, info.height, 4, 4);
@@ -1918,10 +1962,10 @@ async function handleScanStream() {
1918
1962
  }
1919
1963
  async function findOrphans(dir, relativePath = "") {
1920
1964
  try {
1921
- const entries = await fs7.readdir(dir, { withFileTypes: true });
1965
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
1922
1966
  for (const entry of entries) {
1923
1967
  if (entry.name.startsWith(".")) continue;
1924
- const fullPath = path7.join(dir, entry.name);
1968
+ const fullPath = path8.join(dir, entry.name);
1925
1969
  const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1926
1970
  if (entry.isDirectory()) {
1927
1971
  await findOrphans(fullPath, relPath);
@@ -1981,7 +2025,7 @@ async function handleDeleteOrphans(request) {
1981
2025
  }
1982
2026
  const fullPath = getPublicPath(orphanPath);
1983
2027
  try {
1984
- await fs7.unlink(fullPath);
2028
+ await fs8.unlink(fullPath);
1985
2029
  deleted.push(orphanPath);
1986
2030
  } catch (err) {
1987
2031
  console.error(`Failed to delete ${orphanPath}:`, err);
@@ -1991,18 +2035,18 @@ async function handleDeleteOrphans(request) {
1991
2035
  const imagesDir = getPublicPath("images");
1992
2036
  async function removeEmptyDirs(dir) {
1993
2037
  try {
1994
- const entries = await fs7.readdir(dir, { withFileTypes: true });
2038
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
1995
2039
  let isEmpty = true;
1996
2040
  for (const entry of entries) {
1997
2041
  if (entry.isDirectory()) {
1998
- const subDirEmpty = await removeEmptyDirs(path7.join(dir, entry.name));
2042
+ const subDirEmpty = await removeEmptyDirs(path8.join(dir, entry.name));
1999
2043
  if (!subDirEmpty) isEmpty = false;
2000
2044
  } else {
2001
2045
  isEmpty = false;
2002
2046
  }
2003
2047
  }
2004
- if (isEmpty && dir !== imagesDir) {
2005
- await fs7.rmdir(dir);
2048
+ if (isEmpty) {
2049
+ await fs8.rmdir(dir);
2006
2050
  }
2007
2051
  return isEmpty;
2008
2052
  } catch {
@@ -2030,8 +2074,8 @@ import { encode as encode3 } from "blurhash";
2030
2074
  function parseImageUrl(url) {
2031
2075
  const parsed = new URL(url);
2032
2076
  const base = `${parsed.protocol}//${parsed.host}`;
2033
- const path9 = parsed.pathname;
2034
- return { base, path: path9 };
2077
+ const path10 = parsed.pathname;
2078
+ return { base, path: path10 };
2035
2079
  }
2036
2080
  async function processRemoteImage(url) {
2037
2081
  const response = await fetch(url);
@@ -2080,20 +2124,20 @@ async function handleImportUrls(request) {
2080
2124
  currentFile: url
2081
2125
  });
2082
2126
  try {
2083
- const { base, path: path9 } = parseImageUrl(url);
2084
- const existingEntry = getMetaEntry(meta, path9);
2127
+ const { base, path: path10 } = parseImageUrl(url);
2128
+ const existingEntry = getMetaEntry(meta, path10);
2085
2129
  if (existingEntry) {
2086
- skipped.push(path9);
2130
+ skipped.push(path10);
2087
2131
  continue;
2088
2132
  }
2089
2133
  const cdnIndex = getOrAddCdnIndex(meta, base);
2090
2134
  const imageData = await processRemoteImage(url);
2091
- setMetaEntry(meta, path9, {
2135
+ setMetaEntry(meta, path10, {
2092
2136
  o: imageData.o,
2093
2137
  b: imageData.b,
2094
2138
  c: cdnIndex
2095
2139
  });
2096
- added.push(path9);
2140
+ added.push(path10);
2097
2141
  } catch (error) {
2098
2142
  console.error(`Failed to import ${url}:`, error);
2099
2143
  errors.push(url);
@@ -2150,8 +2194,8 @@ async function handleUpdateCdns(request) {
2150
2194
 
2151
2195
  // src/handlers/favicon.ts
2152
2196
  import sharp5 from "sharp";
2153
- import path8 from "path";
2154
- import fs8 from "fs/promises";
2197
+ import path9 from "path";
2198
+ import fs9 from "fs/promises";
2155
2199
  var FAVICON_CONFIGS = [
2156
2200
  { name: "favicon.ico", size: 48 },
2157
2201
  { name: "icon.png", size: 32 },
@@ -2169,7 +2213,7 @@ async function handleGenerateFavicon(request) {
2169
2213
  } catch {
2170
2214
  return jsonResponse({ error: "Invalid request body" }, { status: 400 });
2171
2215
  }
2172
- const fileName = path8.basename(imagePath).toLowerCase();
2216
+ const fileName = path9.basename(imagePath).toLowerCase();
2173
2217
  if (fileName !== "favicon.png" && fileName !== "favicon.jpg") {
2174
2218
  return jsonResponse({
2175
2219
  error: "Source file must be named favicon.png or favicon.jpg"
@@ -2177,7 +2221,7 @@ async function handleGenerateFavicon(request) {
2177
2221
  }
2178
2222
  const sourcePath = getPublicPath(imagePath.replace(/^\//, ""));
2179
2223
  try {
2180
- await fs8.access(sourcePath);
2224
+ await fs9.access(sourcePath);
2181
2225
  } catch {
2182
2226
  return jsonResponse({ error: "Source file not found" }, { status: 404 });
2183
2227
  }
@@ -2189,7 +2233,7 @@ async function handleGenerateFavicon(request) {
2189
2233
  }
2190
2234
  const outputDir = getSrcAppPath();
2191
2235
  try {
2192
- await fs8.access(outputDir);
2236
+ await fs9.access(outputDir);
2193
2237
  } catch {
2194
2238
  return jsonResponse({
2195
2239
  error: "Output directory src/app/ not found"
@@ -2221,7 +2265,7 @@ async function handleGenerateFavicon(request) {
2221
2265
  message: `Generating ${config.name} (${config.size}x${config.size})...`
2222
2266
  });
2223
2267
  try {
2224
- const outputPath = path8.join(outputDir, config.name);
2268
+ const outputPath = path9.join(outputDir, config.name);
2225
2269
  await sharp5(sourcePath).resize(config.size, config.size, {
2226
2270
  fit: "cover",
2227
2271
  position: "center"
@@ -2301,7 +2345,11 @@ async function startServer(options) {
2301
2345
  const htmlPath = join(clientDir, "index.html");
2302
2346
  if (existsSync(htmlPath)) {
2303
2347
  let html = readFileSync(htmlPath, "utf-8");
2304
- const script = `<script>window.__STUDIO_WORKSPACE__ = ${JSON.stringify(workspace)};</script>`;
2348
+ const devUrl = process.env.STUDIO_DEV_URL || "";
2349
+ const script = `<script>
2350
+ window.__STUDIO_WORKSPACE__ = ${JSON.stringify(workspace)};
2351
+ window.__STUDIO_DEV_URL__ = ${JSON.stringify(devUrl)};
2352
+ </script>`;
2305
2353
  html = html.replace("</head>", `${script}</head>`);
2306
2354
  res.type("html").send(html);
2307
2355
  } else {