@hyperframes/studio-server 0.8.81 → 0.8.82

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.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  splitElementInHtml,
14
14
  unwrapElementsFromHtml,
15
15
  wrapElementsInHtml
16
- } from "./chunk-P5EJPIH3.js";
16
+ } from "./chunk-W6TFBTP2.js";
17
17
  import {
18
18
  getElementScreenshotClip
19
19
  } from "./chunk-W2SBTCO2.js";
@@ -583,27 +583,41 @@ import { spawnSync } from "child_process";
583
583
  import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
584
584
  import { tmpdir } from "os";
585
585
  import { basename, join as join4 } from "path";
586
+ import { FFPROBE_PATH_ENV, findFfBinary as findFfBinary2 } from "@hyperframes/parsers/ff-binaries";
586
587
  var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
587
588
  var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
588
- function validateUploadedMedia(filePath, runner = spawnSync) {
589
+ var defaultRunner = spawnSync;
590
+ var FFPROBE_MISSING = "not checked: ffprobe was not found. Install FFmpeg or set HYPERFRAMES_FFPROBE_PATH.";
591
+ function validateUploadedMedia(filePath, runner = defaultRunner) {
589
592
  const isVideo = VIDEO_EXT.test(filePath);
590
593
  const isAudio = AUDIO_EXT.test(filePath);
591
594
  if (!isVideo && !isAudio) {
592
595
  return { ok: true };
593
596
  }
597
+ const ffprobe = findFfBinary2("ffprobe") ?? (runner === defaultRunner ? void 0 : "ffprobe");
598
+ if (!ffprobe) return { ok: true, unchecked: FFPROBE_MISSING };
594
599
  const result = runner(
595
- "ffprobe",
600
+ ffprobe,
596
601
  ["-v", "error", "-show_entries", "stream=codec_type", "-of", "json", "--", filePath],
597
602
  { windowsHide: true }
598
603
  );
604
+ if (result.error && process.env[FFPROBE_PATH_ENV]?.trim()) {
605
+ return {
606
+ ok: false,
607
+ reason: `${FFPROBE_PATH_ENV} names "${ffprobe}", which cannot run: fix it or unset it`
608
+ };
609
+ }
599
610
  if (result.error?.code === "ENOENT") {
600
- return { ok: true };
611
+ return { ok: true, unchecked: FFPROBE_MISSING };
601
612
  }
602
613
  if (result.status !== 0) {
603
614
  return { ok: false, reason: "ffprobe failed to read the media file" };
604
615
  }
616
+ return checkStreams(String(result.stdout || "{}"), isVideo, isAudio);
617
+ }
618
+ function checkStreams(stdout, isVideo, isAudio) {
605
619
  try {
606
- const parsed = JSON.parse(String(result.stdout || "{}"));
620
+ const parsed = JSON.parse(stdout);
607
621
  const streams = parsed.streams ?? [];
608
622
  const hasVideo = streams.some((stream) => stream.codec_type === "video");
609
623
  const hasAudio = streams.some((stream) => stream.codec_type === "audio");
@@ -618,7 +632,7 @@ function validateUploadedMedia(filePath, runner = spawnSync) {
618
632
  return { ok: false, reason: "ffprobe returned unreadable media metadata" };
619
633
  }
620
634
  }
621
- function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
635
+ function validateUploadedMediaBuffer(fileName, buffer, runner = defaultRunner) {
622
636
  const tempDir = mkdtempSync2(join4(tmpdir(), "hyperframes-upload-"));
623
637
  const tempPath = join4(tempDir, basename(fileName));
624
638
  try {
@@ -2502,6 +2516,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2502
2516
  const uploaded = [];
2503
2517
  const skipped = [];
2504
2518
  const invalid = [];
2519
+ const unchecked = [];
2505
2520
  const entries = formData.entries();
2506
2521
  const subDir = targetDir === projectDir ? "" : targetDir.slice(projectDir.length + 1);
2507
2522
  for (const [, value] of entries) {
@@ -2564,12 +2579,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2564
2579
  }
2565
2580
  const relativePath = subDir ? join6(subDir, finalName) : finalName;
2566
2581
  uploaded.push(relativePath);
2582
+ if (validation.unchecked) unchecked.push({ name: finalName, reason: validation.unchecked });
2567
2583
  if (isAudioFile(finalName)) {
2568
2584
  generateWaveformCache(projectDir, relativePath).catch(() => {
2569
2585
  });
2570
2586
  }
2571
2587
  }
2572
- return { uploaded, skipped, invalid };
2588
+ return { uploaded, skipped, invalid, unchecked };
2573
2589
  }
2574
2590
  function registerFileRoutes(api, adapter) {
2575
2591
  api.get("/projects/:id/files/*", async (c) => {
@@ -3301,7 +3317,13 @@ function registerFileRoutes(api, adapter) {
3301
3317
  const formData = await c.req.formData();
3302
3318
  const result = await processUploadedFiles(formData, targetDir, project.dir);
3303
3319
  return c.json(
3304
- { ok: true, files: result.uploaded, skipped: result.skipped, invalid: result.invalid },
3320
+ {
3321
+ ok: true,
3322
+ files: result.uploaded,
3323
+ skipped: result.skipped,
3324
+ invalid: result.invalid,
3325
+ unchecked: result.unchecked
3326
+ },
3305
3327
  201
3306
3328
  );
3307
3329
  }
@@ -3387,6 +3409,8 @@ import {
3387
3409
  insertBeforeCloseTag as insertBeforeCloseTag2,
3388
3410
  stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2
3389
3411
  } from "@hyperframes/core/compiler";
3412
+ import { STUDIO_PREVIEW_MARK_META } from "@hyperframes/core/studio-preview-mark";
3413
+ import { injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
3390
3414
  import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
3391
3415
 
3392
3416
  // src/helpers/subComposition.ts
@@ -3671,6 +3695,34 @@ function injectPreviewVariables(html, values) {
3671
3695
  return tag + html;
3672
3696
  }
3673
3697
 
3698
+ // src/helpers/lazyPreviewImages.ts
3699
+ import { findStartTags } from "@hyperframes/core/compiler/html-document";
3700
+ import { parseHTML as parseHTML4 } from "linkedom";
3701
+ import { STUDIO_PREVIEW_LAZY_ATTR } from "@hyperframes/core/studio-preview-mark";
3702
+ function startsAfterZero(el) {
3703
+ for (let node = el; node; node = node.parentElement) {
3704
+ if (Number(node.getAttribute("data-start")) > 0) return true;
3705
+ }
3706
+ return false;
3707
+ }
3708
+ var hasLoading = (img) => Array.from(img.attributes).some((attr) => attr.name.toLowerCase() === "loading");
3709
+ function lazyPreviewImages(html) {
3710
+ if (!/<!doctype|<html[\s>]/i.test(html)) return html;
3711
+ const images = [...parseHTML4(html).document.querySelectorAll("img")];
3712
+ const tags = findStartTags(html, "img");
3713
+ if (tags.length !== images.length) return html;
3714
+ const parts = [];
3715
+ let from = 0;
3716
+ images.forEach((img, i) => {
3717
+ if (hasLoading(img) || !startsAfterZero(img)) return;
3718
+ const at = (tags[i] ?? 0) + 4;
3719
+ parts.push(html.slice(from, at), ` loading="lazy" ${STUDIO_PREVIEW_LAZY_ATTR}`);
3720
+ from = at;
3721
+ });
3722
+ parts.push(html.slice(from));
3723
+ return parts.join("");
3724
+ }
3725
+
3674
3726
  // src/routes/preview.ts
3675
3727
  var PROJECT_SIGNATURE_META = "hyperframes-project-signature";
3676
3728
  var GSAP_CDN_VERSION = "3.15.0";
@@ -3763,7 +3815,8 @@ function injectStudioMotionScript(html, projectDir, activeCompositionPath) {
3763
3815
  false
3764
3816
  );
3765
3817
  }
3766
- var GSAP_CDN_FALLBACK_SCRIPT = `<script data-hf-gsap-fallback>
3818
+ var GSAP_FALLBACK_ATTR = "data-hf-gsap-fallback";
3819
+ var GSAP_CDN_FALLBACK_SCRIPT = `<script ${GSAP_FALLBACK_ATTR}>
3767
3820
  (function(){
3768
3821
  var cdnBase="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/";
3769
3822
  var loaded={};
@@ -3784,7 +3837,7 @@ var GSAP_CDN_FALLBACK_SCRIPT = `<script data-hf-gsap-fallback>
3784
3837
  })();
3785
3838
  </script>`;
3786
3839
  function injectGsapCdnFallback(html) {
3787
- if (html.includes("data-hf-gsap-fallback")) return html;
3840
+ if (html.includes(GSAP_FALLBACK_ATTR)) return html;
3788
3841
  if (html.includes("<head>")) return html.replace("<head>", "<head>" + GSAP_CDN_FALLBACK_SCRIPT);
3789
3842
  return GSAP_CDN_FALLBACK_SCRIPT + html;
3790
3843
  }
@@ -3810,11 +3863,13 @@ function previewVariablesFromRequest(rawVariables) {
3810
3863
  if (!parse2.ok) return { error: parse2.error };
3811
3864
  return { raw: rawVariables, values: parse2.values };
3812
3865
  }
3813
- function injectStudioPreviewAugmentations(html, adapter, projectDir, activeCompositionPath) {
3866
+ var PREVIEW_CAPTURE_PARAM = "hf-capture";
3867
+ function injectStudioPreviewAugmentations(html, adapter, projectDir, activeCompositionPath, capture) {
3868
+ const marked = capture ? html : injectTagsAtHeadStart(lazyPreviewImages(html), `<meta name="${STUDIO_PREVIEW_MARK_META}">`);
3814
3869
  return injectStudioMotionScript(
3815
3870
  injectMotionPathPluginIfNeeded(
3816
3871
  injectGsapCdnFallback(
3817
- injectProjectSignature(html, resolveProjectSignature(adapter, projectDir))
3872
+ injectProjectSignature(marked, resolveProjectSignature(adapter, projectDir))
3818
3873
  )
3819
3874
  ),
3820
3875
  projectDir,
@@ -3872,7 +3927,7 @@ function registerPreviewRoutes(api, adapter) {
3872
3927
  if (builtPreviews.size > 4) builtPreviews.delete(builtPreviews.keys().next().value);
3873
3928
  };
3874
3929
  const previewBuilds = /* @__PURE__ */ new Map();
3875
- async function buildPreview(project, previewVariables, builtKey) {
3930
+ async function buildPreview(project, previewVariables, builtKey, capture) {
3876
3931
  const diskMain = resolveProjectMainHtml(project.dir, project.id);
3877
3932
  const normalizedDisk = diskMain ? ensureHfIds3(diskMain.html) : null;
3878
3933
  try {
@@ -3902,7 +3957,8 @@ ${runtimeTag}`;
3902
3957
  ensureHfIds3(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
3903
3958
  adapter,
3904
3959
  project.dir,
3905
- mainCompositionPath
3960
+ mainCompositionPath,
3961
+ capture
3906
3962
  );
3907
3963
  if (previewVariables) bundled = injectPreviewVariables(bundled, previewVariables);
3908
3964
  bundled = await injectMediaCodecMap(
@@ -3914,7 +3970,7 @@ ${runtimeTag}`;
3914
3970
  );
3915
3971
  bundled = addScenePartsManifest(bundled, [`meta[name="${PROJECT_SIGNATURE_META}"]`]);
3916
3972
  rememberPreview(builtKey, bundled);
3917
- adapter.previewDocuments?.write(builtKey, bundled);
3973
+ if (!capture) adapter.previewDocuments?.write(builtKey, bundled);
3918
3974
  return bundled;
3919
3975
  } catch {
3920
3976
  const fallback = resolveProjectMainHtml(project.dir, project.id);
@@ -3924,7 +3980,8 @@ ${runtimeTag}`;
3924
3980
  await transformPreviewHtml(fallbackHtml, adapter, project, fallback.compositionPath),
3925
3981
  adapter,
3926
3982
  project.dir,
3927
- fallback.compositionPath
3983
+ fallback.compositionPath,
3984
+ capture
3928
3985
  );
3929
3986
  if (previewVariables) {
3930
3987
  fallbackAugmented = injectPreviewVariables(fallbackAugmented, previewVariables);
@@ -3948,7 +4005,8 @@ ${runtimeTag}`;
3948
4005
  const vars = previewVariablesFromRequest(c.req.query("variables"));
3949
4006
  if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
3950
4007
  const previewVariables = vars.values;
3951
- const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`;
4008
+ const capture = c.req.query(PREVIEW_CAPTURE_PARAM) !== void 0;
4009
+ const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}${capture ? ":capture" : ""}"`;
3952
4010
  const ifNoneMatch = c.req.header("If-None-Match");
3953
4011
  if (ifNoneMatch === etag) {
3954
4012
  return new Response(null, {
@@ -3965,7 +4023,7 @@ ${etag}`;
3965
4023
  }
3966
4024
  let pending = previewBuilds.get(builtKey);
3967
4025
  if (!pending) {
3968
- pending = buildPreview(project, previewVariables, builtKey).finally(
4026
+ pending = buildPreview(project, previewVariables, builtKey, capture).finally(
3969
4027
  () => previewBuilds.delete(builtKey)
3970
4028
  );
3971
4029
  previewBuilds.set(builtKey, pending);
@@ -3997,7 +4055,8 @@ ${etag}`;
3997
4055
  return c.text("not found", 404);
3998
4056
  }
3999
4057
  const compPathHash = createHash4("sha1").update(compPath).digest("hex");
4000
- const etag = `"comp:v2:${compPathHash}:${signature}${variablesEtagSalt(vars.raw)}"`;
4058
+ const capture = c.req.query(PREVIEW_CAPTURE_PARAM) !== void 0;
4059
+ const etag = `"comp:v2:${compPathHash}:${signature}${variablesEtagSalt(vars.raw)}${capture ? ":capture" : ""}"`;
4001
4060
  const ifNoneMatch = c.req.header("If-None-Match");
4002
4061
  if (ifNoneMatch === etag) {
4003
4062
  return new Response(null, {
@@ -4018,7 +4077,7 @@ ${etag}`;
4018
4077
  if (!html) return c.text("not found", 404);
4019
4078
  recordPreviewReferences(project.dir, html);
4020
4079
  html = ensureHfIds3(await transformPreviewHtml(html, adapter, project, compPath));
4021
- html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath);
4080
+ html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath, capture);
4022
4081
  if (previewVariables) html = injectPreviewVariables(html, previewVariables);
4023
4082
  html = await injectMediaCodecMap(html, adapter, project.dir, compPath, mediaCodecProbeCache);
4024
4083
  return c.html(html, 200, previewCacheHeaders(etag));
@@ -4817,7 +4876,8 @@ function registerThumbnailRoutes(api, adapter) {
4817
4876
  const motion = manifestKey(join12(project.dir, STUDIO_MOTION_PATH));
4818
4877
  sourceMtime = Math.max(sourceMtime, manualEdits.mtimeMs, motion.mtimeMs);
4819
4878
  const projectUrl = `http://${c.req.header("host")}/api/projects/${encodeURIComponent(project.id)}`;
4820
- const previewUrl = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
4879
+ const previewPath = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
4880
+ const previewUrl = `${previewPath}?${PREVIEW_CAPTURE_PARAM}=1`;
4821
4881
  const cacheDir = join12(project.dir, ".thumbnails");
4822
4882
  const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
4823
4883
  const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
@@ -5194,7 +5254,7 @@ function registerSelectionRoutes(api, adapter) {
5194
5254
  // src/routes/media.ts
5195
5255
  import { streamSSE as streamSSE2 } from "hono/streaming";
5196
5256
  import { existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
5197
- import { basename as basename2, dirname as dirname6, extname as extname2, join as join14 } from "path";
5257
+ import { basename as basename2, dirname as dirname6, extname as extname2, join as join14, posix } from "path";
5198
5258
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
5199
5259
  ".mp4",
5200
5260
  ".mov",
@@ -5227,21 +5287,31 @@ function slugFileBase(path) {
5227
5287
  const name = basename2(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
5228
5288
  return name || "media";
5229
5289
  }
5230
- function uniqueAssetPath(projectDir, assetPath) {
5290
+ function uniqueAssetPath(projectDir, assetPath, taken) {
5231
5291
  const ext = extname2(assetPath);
5232
5292
  const withoutExt = assetPath.slice(0, -ext.length);
5233
5293
  let candidate = assetPath;
5234
- for (let index = 2; existsSync8(join14(projectDir, candidate)); index++) {
5294
+ for (let index = 2; taken.has(candidate) || existsSync8(join14(projectDir, candidate)); index++) {
5235
5295
  candidate = `${withoutExt}-${index}${ext}`;
5236
5296
  }
5237
5297
  return candidate;
5238
5298
  }
5239
- function defaultOutputPath(projectDir, inputPath) {
5299
+ function defaultOutputPath(projectDir, inputPath, taken) {
5240
5300
  const ext = isImagePath(inputPath) ? ".png" : ".webm";
5241
- return uniqueAssetPath(projectDir, `assets/cutouts/${slugFileBase(inputPath)}-cutout${ext}`);
5301
+ return uniqueAssetPath(
5302
+ projectDir,
5303
+ `assets/cutouts/${slugFileBase(inputPath)}-cutout${ext}`,
5304
+ taken
5305
+ );
5242
5306
  }
5243
- function defaultPlatePath(projectDir, inputPath) {
5244
- return uniqueAssetPath(projectDir, `assets/cutouts/${slugFileBase(inputPath)}-plate.webm`);
5307
+ function defaultPlatePath(projectDir, inputPath, taken) {
5308
+ return uniqueAssetPath(projectDir, `assets/cutouts/${slugFileBase(inputPath)}-plate.webm`, taken);
5309
+ }
5310
+ function outputsInFlight(mediaJobs) {
5311
+ const running = [...mediaJobs.values()].filter((job) => job.status === "processing");
5312
+ return new Set(
5313
+ running.flatMap((job) => [job.outputAssetPath, job.backgroundOutputAssetPath ?? ""])
5314
+ );
5245
5315
  }
5246
5316
  function makeJobId(projectId, mediaJobs) {
5247
5317
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
@@ -5310,15 +5380,16 @@ function registerMediaRoutes(api, adapter, options = {}) {
5310
5380
  if (!inputIsVideo && !inputIsImage) {
5311
5381
  return c.json({ error: "background removal supports video or image assets only" }, 400);
5312
5382
  }
5313
- const requestedOutput = body.outputPath ? normalizeProjectAssetPath(body.outputPath) : "";
5383
+ const requestedOutput = body.outputPath ? posix.normalize(normalizeProjectAssetPath(body.outputPath)) : "";
5314
5384
  if (requestedOutput && containsNullByte(requestedOutput)) {
5315
5385
  return c.json({ error: "forbidden" }, 403);
5316
5386
  }
5317
5387
  if (requestedOutput && !resolveWithinProject(project.dir, requestedOutput)) {
5318
5388
  return c.json({ error: "forbidden" }, 403);
5319
5389
  }
5320
- const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput) : defaultOutputPath(project.dir, inputAssetPath);
5321
- const outputPath = resolveWithinProject(project.dir, outputAssetPath);
5390
+ const taken = outputsInFlight(mediaJobs);
5391
+ const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput, taken) : defaultOutputPath(project.dir, inputAssetPath, taken);
5392
+ const outputPath = pinWithinProject(project.dir, outputAssetPath);
5322
5393
  if (!outputPath) return c.json({ error: "forbidden" }, 403);
5323
5394
  if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname2(outputAssetPath).toLowerCase())) {
5324
5395
  return c.json({ error: "video background removal output must be .webm or .mov" }, 400);
@@ -5332,8 +5403,9 @@ function registerMediaRoutes(api, adapter, options = {}) {
5332
5403
  if (!inputIsVideo) {
5333
5404
  return c.json({ error: "background plates are only supported for video inputs" }, 400);
5334
5405
  }
5335
- backgroundOutputAssetPath = defaultPlatePath(project.dir, inputAssetPath);
5336
- backgroundOutputPath = resolveWithinProject(project.dir, backgroundOutputAssetPath) ?? void 0;
5406
+ taken.add(outputAssetPath);
5407
+ backgroundOutputAssetPath = defaultPlatePath(project.dir, inputAssetPath, taken);
5408
+ backgroundOutputPath = pinWithinProject(project.dir, backgroundOutputAssetPath) ?? void 0;
5337
5409
  if (!backgroundOutputPath) {
5338
5410
  return c.json({ error: "forbidden" }, 403);
5339
5411
  }
@@ -5421,20 +5493,20 @@ function registerGlobalAssetRoutes(api) {
5421
5493
  }
5422
5494
 
5423
5495
  // src/history/projectHistory.ts
5424
- import { randomUUID as randomUUID8 } from "crypto";
5425
- import { readdir as readdir2, rm as rm2, rmdir, writeFile } from "fs/promises";
5496
+ import { randomUUID as randomUUID9 } from "crypto";
5497
+ import { readdir as readdir3, rm as rm3, rmdir, writeFile } from "fs/promises";
5426
5498
  import {
5427
5499
  existsSync as existsSync11,
5428
5500
  lstatSync as lstatSync4,
5429
- statSync as statSync9,
5501
+ statSync as statSync10,
5430
5502
  mkdirSync as mkdirSync11,
5431
- readdirSync as readdirSync6,
5432
- readFileSync as readFileSync18,
5503
+ readdirSync as readdirSync7,
5504
+ readFileSync as readFileSync19,
5433
5505
  realpathSync,
5434
5506
  rmSync as rmSync6
5435
5507
  } from "fs";
5436
5508
  import { homedir as homedir2 } from "os";
5437
- import { dirname as dirname9, isAbsolute as isAbsolute2, join as join19, relative as relative5, resolve as resolve8, sep as sep3 } from "path";
5509
+ import { dirname as dirname10, isAbsolute as isAbsolute2, join as join20, relative as relative5, resolve as resolve8, sep as sep3 } from "path";
5438
5510
 
5439
5511
  // src/history/blobStore.ts
5440
5512
  import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
@@ -5503,12 +5575,20 @@ async function openBlobStore(dir) {
5503
5575
  };
5504
5576
  }
5505
5577
 
5578
+ // src/history/pruneHistories.ts
5579
+ import { randomUUID as randomUUID8 } from "crypto";
5580
+ import { readdirSync as readdirSync6, readFileSync as readFileSync17, renameSync as renameSync5, statSync as statSync9, writeFileSync as writeFileSync9 } from "fs";
5581
+ import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
5582
+ import { tmpdir as tmpdir2 } from "os";
5583
+ import { dirname as dirname8, join as join19 } from "path";
5584
+
5506
5585
  // src/history/historyId.ts
5507
5586
  import { randomUUID as randomUUID6 } from "crypto";
5508
5587
  import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync15, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
5509
5588
  import { basename as basename3, join as join17, resolve as resolve7 } from "path";
5510
5589
  var ID_PATH = join17(".hyperframes", "history-id");
5511
5590
  var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
5591
+ var isHistoryId = (name) => ID_SHAPE.test(name);
5512
5592
  function readId(projectDir) {
5513
5593
  try {
5514
5594
  const id = readFileSync15(join17(projectDir, ID_PATH), "utf-8").trim();
@@ -5566,7 +5646,7 @@ var HistoryIdError = class extends Error {
5566
5646
  }
5567
5647
  };
5568
5648
  function recordProject(historyDir, dir, folder) {
5569
- const record = { dir, ino: folder.ino, born: folder.birthtimeMs };
5649
+ const record = { dir, ino: folder.ino, born: folder.birthtimeMs, dev: folder.dev };
5570
5650
  mkdirSync8(historyDir, { recursive: true });
5571
5651
  replaceFileAtomically(join17(historyDir, "project.json"), JSON.stringify(record), 420);
5572
5652
  }
@@ -5652,9 +5732,145 @@ async function takeHistoryOwnership(home, waitMs) {
5652
5732
  }
5653
5733
  }
5654
5734
 
5735
+ // src/history/pruneHistories.ts
5736
+ var DAY_MS = 24 * 60 * 60 * 1e3;
5737
+ var KEEP_GONE_PROJECT_HISTORY_MS = 14 * DAY_MS;
5738
+ var PRUNED_PREFIX = ".pruned-";
5739
+ var LAST_PRUNE_FILE = ".last-prune";
5740
+ async function pruneGoneProjectHistories(historyRoot, options = {}) {
5741
+ return (await pruneWithin(historyRoot, options)).pruned;
5742
+ }
5743
+ async function pruneWithin(historyRoot, {
5744
+ now = Date.now(),
5745
+ dryRun = false,
5746
+ tempDir = tmpdir2(),
5747
+ onError = (error) => console.warn("[history] could not prune a history:", error),
5748
+ budgetMs = Infinity,
5749
+ measure = true
5750
+ }) {
5751
+ const started = performance.now();
5752
+ let names;
5753
+ try {
5754
+ names = readdirSync6(historyRoot);
5755
+ } catch (error) {
5756
+ if (error.code === "ENOENT") return { pruned: [], finished: true };
5757
+ throw error;
5758
+ }
5759
+ const pruned = [];
5760
+ for (const name of names) {
5761
+ if (performance.now() - started >= budgetMs) return { pruned, finished: false };
5762
+ try {
5763
+ const gone = await pruneEntry(historyRoot, name, { now, dryRun, tempDir, measure });
5764
+ if (gone) pruned.push(gone);
5765
+ } catch (error) {
5766
+ onError(error);
5767
+ }
5768
+ }
5769
+ return { pruned, finished: true };
5770
+ }
5771
+ async function pruneEntry(historyRoot, name, run) {
5772
+ if (name.startsWith(PRUNED_PREFIX)) {
5773
+ if (!run.dryRun) await rm2(join19(historyRoot, name), { recursive: true, force: true });
5774
+ return null;
5775
+ }
5776
+ return isHistoryId(name) ? pruneOne(historyRoot, name, run) : null;
5777
+ }
5778
+ async function pruneOne(historyRoot, id, run) {
5779
+ const home = join19(historyRoot, id);
5780
+ if (abandonedProject(home, id, run.now, run.tempDir) === null) return null;
5781
+ const release = await takeUnlessBusy(home);
5782
+ if (!release) return null;
5783
+ let taken;
5784
+ try {
5785
+ taken = recheckAndMoveAway(historyRoot, id, run);
5786
+ } catch (error) {
5787
+ release();
5788
+ throw error;
5789
+ }
5790
+ if (!taken?.trash) release();
5791
+ if (!taken) return null;
5792
+ const bytes = await clearAway(taken.trash, home, run.measure);
5793
+ return { id, projectDir: taken.projectDir, bytes };
5794
+ }
5795
+ async function clearAway(trash, home, measure) {
5796
+ const bytes = measure ? await bytesIn(trash ?? home) : 0;
5797
+ if (trash) await rm2(trash, { recursive: true, force: true });
5798
+ return bytes;
5799
+ }
5800
+ async function takeUnlessBusy(home) {
5801
+ try {
5802
+ return await takeHistoryOwnership(home, 0);
5803
+ } catch (error) {
5804
+ if (error instanceof HistoryBusyError) return null;
5805
+ throw error;
5806
+ }
5807
+ }
5808
+ function recheckAndMoveAway(historyRoot, id, { now, dryRun, tempDir }) {
5809
+ const home = join19(historyRoot, id);
5810
+ const projectDir = abandonedProject(home, id, now, tempDir);
5811
+ if (projectDir === null) return null;
5812
+ if (dryRun) return { projectDir, trash: null };
5813
+ const trash = join19(historyRoot, `${PRUNED_PREFIX}${id}-${randomUUID8()}`);
5814
+ renameSync5(home, trash);
5815
+ return { projectDir, trash };
5816
+ }
5817
+ function abandonedProject(home, id, now, tempDir) {
5818
+ const record = readRecord(home);
5819
+ const dir = record?.dir;
5820
+ if (typeof dir !== "string") return null;
5821
+ if (now - lastUsed(home) < KEEP_GONE_PROJECT_HISTORY_MS && !isSafePath(tempDir, dir)) return null;
5822
+ return projectGone(dir, id, record?.dev, tempDir) ? dir : null;
5823
+ }
5824
+ function projectGone(dir, id, dev, tempDir) {
5825
+ if (!statSync9(join19(dir, ID_PATH), { throwIfNoEntry: false }))
5826
+ return diskStillHere(dir, dev, tempDir);
5827
+ const there = readId(dir);
5828
+ return there !== null && there !== id;
5829
+ }
5830
+ function diskStillHere(dir, dev, tempDir) {
5831
+ let at = dir;
5832
+ let stat3 = statSync9(at, { throwIfNoEntry: false });
5833
+ while (!stat3) {
5834
+ if (dirname8(at) === at) return false;
5835
+ at = dirname8(at);
5836
+ stat3 = statSync9(at, { throwIfNoEntry: false });
5837
+ }
5838
+ if (typeof dev === "number") return stat3.dev === dev;
5839
+ return at === dir || isSafePath(tempDir, at);
5840
+ }
5841
+ function lastUsed(home) {
5842
+ const mtime = (file) => statSync9(join19(home, file), { throwIfNoEntry: false })?.mtimeMs ?? 0;
5843
+ return Math.max(mtime("project.json"), mtime("log.jsonl"));
5844
+ }
5845
+ async function bytesIn(dir) {
5846
+ let total = 0;
5847
+ for (const entry of await readdir2(dir, { withFileTypes: true })) {
5848
+ const path = join19(dir, entry.name);
5849
+ total += entry.isDirectory() ? await bytesIn(path) : (await lstat(path)).size;
5850
+ }
5851
+ return total;
5852
+ }
5853
+ function prunedWithinADay(stamp, now) {
5854
+ try {
5855
+ const last = Number(readFileSync17(stamp, "utf-8"));
5856
+ return last <= now && now - last < DAY_MS;
5857
+ } catch {
5858
+ return false;
5859
+ }
5860
+ }
5861
+ var pruning = /* @__PURE__ */ new Set();
5862
+ function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs } = {}) {
5863
+ const stamp = join19(historyRoot, LAST_PRUNE_FILE);
5864
+ if (pruning.has(historyRoot) || prunedWithinADay(stamp, now)) return;
5865
+ pruning.add(historyRoot);
5866
+ setImmediate(() => {
5867
+ pruneWithin(historyRoot, { now, onError, budgetMs, measure: false }).then(({ finished }) => finished && writeFileSync9(stamp, String(now))).catch((error) => onError?.(error)).finally(() => pruning.delete(historyRoot));
5868
+ });
5869
+ }
5870
+
5655
5871
  // src/history/historyLog.ts
5656
- import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync10, openSync as openSync5, readFileSync as readFileSync17, writeSync as writeSync2 } from "fs";
5657
- import { dirname as dirname8 } from "path";
5872
+ import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync10, openSync as openSync5, readFileSync as readFileSync18, writeSync as writeSync2 } from "fs";
5873
+ import { dirname as dirname9 } from "path";
5658
5874
  var START = "start";
5659
5875
  function parseRecord(line) {
5660
5876
  try {
@@ -5666,7 +5882,7 @@ function parseRecord(line) {
5666
5882
  function readLog(file, onUnreadable) {
5667
5883
  let text2;
5668
5884
  try {
5669
- text2 = readFileSync17(file, "utf-8");
5885
+ text2 = readFileSync18(file, "utf-8");
5670
5886
  } catch {
5671
5887
  return null;
5672
5888
  }
@@ -5706,7 +5922,7 @@ function writeLog(file, log) {
5706
5922
  ...log.entries.map((entry) => ({ type: "entry", entry })),
5707
5923
  ...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
5708
5924
  ];
5709
- mkdirSync10(dirname8(file), { recursive: true });
5925
+ mkdirSync10(dirname9(file), { recursive: true });
5710
5926
  replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
5711
5927
  `).join(""), 420);
5712
5928
  }
@@ -5779,7 +5995,7 @@ function referencedHashes(log, current) {
5779
5995
  }
5780
5996
 
5781
5997
  // src/history/projectHistory.ts
5782
- var DEFAULT_HISTORY_ROOT = join19(homedir2(), ".cache", "hyperframes", "history");
5998
+ var DEFAULT_HISTORY_ROOT = join20(homedir2(), ".cache", "hyperframes", "history");
5783
5999
  var MAX_WINDOW_IDLE_MS = 10 * 6e4;
5784
6000
  var UNDO_MODES = ["just-this", "back-to-before", "keep-later-edits"];
5785
6001
  var OUTSIDE = { kind: "outside", name: "Outside" };
@@ -5799,26 +6015,26 @@ var isMissingFolder = (error) => ["ENOENT", "ENOTDIR"].includes(error.code ?? ""
5799
6015
  async function removeEmptyFolders(dir) {
5800
6016
  let entries;
5801
6017
  try {
5802
- entries = await readdir2(dir, { withFileTypes: true });
6018
+ entries = await readdir3(dir, { withFileTypes: true });
5803
6019
  } catch (error) {
5804
6020
  if (isMissingFolder(error)) return;
5805
6021
  throw error;
5806
6022
  }
5807
6023
  for (const entry of entries)
5808
- if (entry.isDirectory()) await removeEmptyFolders(join19(dir, entry.name));
6024
+ if (entry.isDirectory()) await removeEmptyFolders(join20(dir, entry.name));
5809
6025
  await rmdir(dir);
5810
6026
  }
5811
6027
  function fileLeftIn(dir, at, deleted) {
5812
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
6028
+ for (const entry of readdirSync7(dir, { withFileTypes: true })) {
5813
6029
  const path = `${at}/${entry.name}`;
5814
- const left = entry.isDirectory() ? fileLeftIn(join19(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
6030
+ const left = entry.isDirectory() ? fileLeftIn(join20(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
5815
6031
  if (left) return left;
5816
6032
  }
5817
6033
  return void 0;
5818
6034
  }
5819
6035
  function ignoresCase(dir) {
5820
- const self = statSync9(join19(dir, ID_PATH), { throwIfNoEntry: false });
5821
- const other = statSync9(join19(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
6036
+ const self = statSync10(join20(dir, ID_PATH), { throwIfNoEntry: false });
6037
+ const other = statSync10(join20(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
5822
6038
  return self !== void 0 && other?.ino === self.ino && other.dev === self.dev;
5823
6039
  }
5824
6040
  var blocks = (removed, added) => added.startsWith(`${removed}/`) || removed.startsWith(`${added}/`);
@@ -5835,9 +6051,9 @@ var Engine = class {
5835
6051
  this.projectId = projectId;
5836
6052
  this.blobs = blobs;
5837
6053
  this.dir = resolve8(options.projectDir);
5838
- this.home = join19(options.historyRoot, projectId);
6054
+ this.home = join20(options.historyRoot, projectId);
5839
6055
  this.foldsCase = ignoresCase(this.dir);
5840
- const folder = statSync9(this.dir, { throwIfNoEntry: false });
6056
+ const folder = statSync10(this.dir, { throwIfNoEntry: false });
5841
6057
  if (!folder || readId(this.dir) !== projectId || !isRecordedFolder(this.home, folder))
5842
6058
  throw this.replaced();
5843
6059
  this.folder = { dev: folder.dev, ino: folder.ino, birthtimeMs: folder.birthtimeMs };
@@ -5868,7 +6084,7 @@ var Engine = class {
5868
6084
  return this.foldsCase ? path.toLowerCase() : path;
5869
6085
  }
5870
6086
  get logFile() {
5871
- return join19(this.home, "log.jsonl");
6087
+ return join20(this.home, "log.jsonl");
5872
6088
  }
5873
6089
  now() {
5874
6090
  return (this.options.now ?? Date.now)();
@@ -5883,7 +6099,7 @@ var Engine = class {
5883
6099
  }
5884
6100
  /** Another project if the folder or its id changed, or the id is gone: a recreated folder may reuse the inode. */
5885
6101
  whereFolder() {
5886
- const now = statSync9(this.dir, { throwIfNoEntry: false });
6102
+ const now = statSync10(this.dir, { throwIfNoEntry: false });
5887
6103
  if (!now) return "gone";
5888
6104
  const same = now.dev === this.folder.dev && sameFolder(now, this.folder);
5889
6105
  return same && readId(this.dir) === this.projectId ? "here" : "replaced";
@@ -5896,7 +6112,7 @@ var Engine = class {
5896
6112
  persistLog(record) {
5897
6113
  if (record) saveRecord(this.logFile, this.log, record);
5898
6114
  else writeLog(this.logFile, this.log);
5899
- if (!existsSync11(join19(this.home, "project.json")))
6115
+ if (!existsSync11(join20(this.home, "project.json")))
5900
6116
  recordProject(this.home, this.dir, this.folder);
5901
6117
  }
5902
6118
  replaced() {
@@ -5946,7 +6162,7 @@ var Engine = class {
5946
6162
  }
5947
6163
  readStatCache() {
5948
6164
  try {
5949
- const files = JSON.parse(readFileSync18(join19(this.home, "stat.json"), "utf-8"));
6165
+ const files = JSON.parse(readFileSync19(join20(this.home, "stat.json"), "utf-8"));
5950
6166
  return new Map(Object.entries(files));
5951
6167
  } catch {
5952
6168
  return /* @__PURE__ */ new Map();
@@ -5955,7 +6171,7 @@ var Engine = class {
5955
6171
  saveStatCache() {
5956
6172
  const files = JSON.stringify(Object.fromEntries(this.tracked));
5957
6173
  mkdirSync11(this.home, { recursive: true });
5958
- replaceFileAtomically(join19(this.home, "stat.json"), files, 420);
6174
+ replaceFileAtomically(join20(this.home, "stat.json"), files, 420);
5959
6175
  }
5960
6176
  /**
5961
6177
  * Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
@@ -5995,7 +6211,7 @@ var Engine = class {
5995
6211
  /** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
5996
6212
  async storeIfPresent(path) {
5997
6213
  try {
5998
- return await this.blobs.put(join19(this.dir, path));
6214
+ return await this.blobs.put(join20(this.dir, path));
5999
6215
  } catch (error) {
6000
6216
  if (error.code === "ENOENT") return null;
6001
6217
  throw error;
@@ -6082,7 +6298,7 @@ var Engine = class {
6082
6298
  }
6083
6299
  hits.forEach(
6084
6300
  ({ change }, i) => forgetOverwrittenBytes(
6085
- join19(this.dir, change.path),
6301
+ join20(this.dir, change.path),
6086
6302
  new Set([...walked[i]].map(hashVersion))
6087
6303
  )
6088
6304
  );
@@ -6090,7 +6306,7 @@ var Engine = class {
6090
6306
  }
6091
6307
  /** Walks the server's writes back from `change.after` to the bytes they replaced, stored so an undo restores them. */
6092
6308
  async overwrittenBy(change, told, seen) {
6093
- const absPath = join19(this.dir, change.path);
6309
+ const absPath = join20(this.dir, change.path);
6094
6310
  let bytes;
6095
6311
  for (let hash = change.after; hash; ) {
6096
6312
  if (seen.has(hash)) return void 0;
@@ -6102,12 +6318,12 @@ var Engine = class {
6102
6318
  if (hash === change.before || hash === told) return hash;
6103
6319
  }
6104
6320
  if (bytes === void 0) return void 0;
6105
- const staged = join19(this.home, `overwrote-${randomUUID8()}`);
6321
+ const staged = join20(this.home, `overwrote-${randomUUID9()}`);
6106
6322
  await writeFile(staged, bytes);
6107
6323
  try {
6108
6324
  return await this.blobs.put(staged);
6109
6325
  } finally {
6110
- await rm2(staged, { force: true });
6326
+ await rm3(staged, { force: true });
6111
6327
  }
6112
6328
  }
6113
6329
  cutOut(group, change, cut) {
@@ -6157,7 +6373,7 @@ var Engine = class {
6157
6373
  return this.outside;
6158
6374
  }
6159
6375
  newGroup(who, label) {
6160
- return { id: randomUUID8(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
6376
+ return { id: randomUUID9(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
6161
6377
  }
6162
6378
  async commitOutside() {
6163
6379
  if (this.outside?.changes.size) await this.commitClaim();
@@ -6274,7 +6490,7 @@ var Engine = class {
6274
6490
  const parts = path.split("/");
6275
6491
  for (let depth = 1; depth <= parts.length; depth += 1) {
6276
6492
  const at = parts.slice(0, depth).join("/");
6277
- const stat3 = lstatSync4(join19(this.dir, at), { throwIfNoEntry: false });
6493
+ const stat3 = lstatSync4(join20(this.dir, at), { throwIfNoEntry: false });
6278
6494
  if (!stat3) return void 0;
6279
6495
  if (at === path || !stat3.isDirectory()) return { at, stat: stat3 };
6280
6496
  }
@@ -6286,7 +6502,7 @@ var Engine = class {
6286
6502
  if (!standing) return void 0;
6287
6503
  if (standing.stat.isSymbolicLink()) return standing.at;
6288
6504
  if (standing.at === path && !standing.stat.isDirectory()) return void 0;
6289
- const real = realpathSync.native(join19(this.dir, standing.at));
6505
+ const real = realpathSync.native(join20(this.dir, standing.at));
6290
6506
  const at = relative5(realpathSync.native(this.dir), real).split(sep3).join("/");
6291
6507
  if (standing.at !== path) return deleted.has(at) ? void 0 : at;
6292
6508
  return fileLeftIn(real, at, deleted);
@@ -6311,7 +6527,7 @@ var Engine = class {
6311
6527
  /** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
6312
6528
  async writeProjectFile(path, hash) {
6313
6529
  this.assertWritable();
6314
- const absPath = join19(this.dir, path);
6530
+ const absPath = join20(this.dir, path);
6315
6531
  const { writeToken } = this;
6316
6532
  if (writeToken) {
6317
6533
  const version = hash === null ? DELETED_VERSION : hashVersion(hash);
@@ -6445,14 +6661,14 @@ var Engine = class {
6445
6661
  const dest = resolve8(emptyDir);
6446
6662
  if (isWithin(this.dir, dest))
6447
6663
  throw new Error(`Checkout writes outside the project only: ${dest}`);
6448
- if ((await readdir2(dest).catch(missingIsEmpty)).length > 0)
6664
+ if ((await readdir3(dest).catch(missingIsEmpty)).length > 0)
6449
6665
  throw new Error(`Checkout writes into an empty folder only: ${dest}`);
6450
6666
  const files = manifestAround(this.log, entryId, side);
6451
6667
  try {
6452
- for (const [path, hash] of files) await this.blobs.writeTo(hash, join19(dest, path));
6668
+ for (const [path, hash] of files) await this.blobs.writeTo(hash, join20(dest, path));
6453
6669
  } catch (error) {
6454
- for (const name of await readdir2(dest).catch(() => []))
6455
- await rm2(join19(dest, name), { recursive: true, force: true });
6670
+ for (const name of await readdir3(dest).catch(() => []))
6671
+ await rm3(join20(dest, name), { recursive: true, force: true });
6456
6672
  throw error;
6457
6673
  }
6458
6674
  }),
@@ -6488,7 +6704,7 @@ var Engine = class {
6488
6704
  };
6489
6705
  function isWithin(dir, path) {
6490
6706
  let probe = resolve8(path);
6491
- while (!existsSync11(probe) && dirname9(probe) !== probe) probe = dirname9(probe);
6707
+ while (!existsSync11(probe) && dirname10(probe) !== probe) probe = dirname10(probe);
6492
6708
  const rel = relative5(realpathSync.native(dir), realpathSync.native(probe));
6493
6709
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute2(rel);
6494
6710
  }
@@ -6507,13 +6723,17 @@ async function openProjectHistory(options) {
6507
6723
  throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
6508
6724
  const projectId = projectHistoryId(options.projectDir, options.historyRoot);
6509
6725
  const release = await takeHistoryOwnership(
6510
- join19(options.historyRoot, projectId),
6726
+ join20(options.historyRoot, projectId),
6511
6727
  options.ownerWaitMs ?? 5e3
6512
6728
  );
6513
6729
  try {
6514
- const blobs = await openBlobStore(join19(options.historyRoot, projectId, "blobs"));
6730
+ const blobs = await openBlobStore(join20(options.historyRoot, projectId, "blobs"));
6515
6731
  const engine = new Engine(options, projectId, blobs);
6516
6732
  await engine.queue(() => engine.start());
6733
+ pruneGoneProjectHistoriesDaily(options.historyRoot, engine.now(), {
6734
+ onError: options.onError,
6735
+ budgetMs: options.pruneGoneProjectsBudgetMs
6736
+ });
6517
6737
  const api = engine.api();
6518
6738
  return {
6519
6739
  ...api,
@@ -6760,15 +6980,15 @@ function numericAttribute(element, name) {
6760
6980
 
6761
6981
  // src/helpers/previewDocumentStore.ts
6762
6982
  import { createHash as createHash8 } from "crypto";
6763
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync7, rmSync as rmSync7 } from "fs";
6764
- import { join as join20 } from "path";
6983
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync20, readdirSync as readdirSync8, rmSync as rmSync7 } from "fs";
6984
+ import { join as join21 } from "path";
6765
6985
  function createPreviewDocumentStore(dir, salt) {
6766
- const fileFor = (key) => join20(dir, `${createHash8("sha256").update(`${salt}
6986
+ const fileFor = (key) => join21(dir, `${createHash8("sha256").update(`${salt}
6767
6987
  ${key}`).digest("hex").slice(0, 32)}.html`);
6768
6988
  return {
6769
6989
  read(key) {
6770
6990
  try {
6771
- return readFileSync19(fileFor(key), "utf-8");
6991
+ return readFileSync20(fileFor(key), "utf-8");
6772
6992
  } catch {
6773
6993
  return null;
6774
6994
  }
@@ -6778,8 +6998,8 @@ ${key}`).digest("hex").slice(0, 32)}.html`);
6778
6998
  mkdirSync12(dir, { recursive: true });
6779
6999
  const file = fileFor(key);
6780
7000
  replaceFileAtomically(file, html, 420);
6781
- for (const name of readdirSync7(dir)) {
6782
- if (join20(dir, name) !== file) rmSync7(join20(dir, name), { force: true });
7001
+ for (const name of readdirSync8(dir)) {
7002
+ if (join21(dir, name) !== file) rmSync7(join21(dir, name), { force: true });
6783
7003
  }
6784
7004
  } catch {
6785
7005
  }
@@ -6892,6 +7112,7 @@ export {
6892
7112
  openProjectHistory,
6893
7113
  parseSourceDocument,
6894
7114
  patchElementInHtml,
7115
+ pruneGoneProjectHistories,
6895
7116
  removeElementFromHtml,
6896
7117
  requestSubPath,
6897
7118
  settledFileTag,