@hyperframes/studio-server 0.8.81 → 0.8.83
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/{chunk-G6IIFKXY.js → chunk-EL3UQI46.js} +21 -15
- package/dist/chunk-EL3UQI46.js.map +1 -0
- package/dist/{chunk-FTDTO3QP.js → chunk-HWBORC2U.js} +3 -3
- package/dist/{chunk-P5EJPIH3.js → chunk-W6TFBTP2.js} +7 -6
- package/dist/chunk-W6TFBTP2.js.map +1 -0
- package/dist/{chunk-TJQ76IEA.js → chunk-WQ2WF2YJ.js} +29 -3
- package/dist/chunk-WQ2WF2YJ.js.map +1 -0
- package/dist/helpers/mediaCodecMap.js +1 -1
- package/dist/helpers/mediaProxyPreview.d.ts +1 -1
- package/dist/helpers/mediaProxyPreview.js +3 -3
- package/dist/helpers/proxyTranscoder.d.ts +1 -1
- package/dist/helpers/proxyTranscoder.js +2 -2
- package/dist/helpers/sourceMutation.js +1 -1
- package/dist/index.d.ts +32 -6
- package/dist/index.js +386 -128
- package/dist/index.js.map +1 -1
- package/dist/{mediaProxyPreview-BOp3Wa9v.d.ts → mediaProxyPreview-BMHd9G3N.d.ts} +1 -0
- package/package.json +3 -3
- package/dist/chunk-G6IIFKXY.js.map +0 -1
- package/dist/chunk-P5EJPIH3.js.map +0 -1
- package/dist/chunk-TJQ76IEA.js.map +0 -1
- /package/dist/{chunk-FTDTO3QP.js.map → chunk-HWBORC2U.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
splitElementInHtml,
|
|
14
14
|
unwrapElementsFromHtml,
|
|
15
15
|
wrapElementsInHtml
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-W6TFBTP2.js";
|
|
17
17
|
import {
|
|
18
18
|
getElementScreenshotClip
|
|
19
19
|
} from "./chunk-W2SBTCO2.js";
|
|
@@ -22,18 +22,21 @@ import {
|
|
|
22
22
|
isAutoProxyEnabled,
|
|
23
23
|
proxyEtagSalt,
|
|
24
24
|
resolvePreviewMediaCodecProbeCache
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-HWBORC2U.js";
|
|
26
26
|
import {
|
|
27
27
|
ProxyCapacityError,
|
|
28
28
|
ProxyTranscodeError,
|
|
29
29
|
resolveProxy
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-EL3UQI46.js";
|
|
31
31
|
import {
|
|
32
32
|
PROXY_VARIANT_CONFIG,
|
|
33
33
|
decideMediaProxyEligibility,
|
|
34
|
+
folderGone,
|
|
34
35
|
isInHiddenOrVendorDir,
|
|
36
|
+
isProjectRootMissing,
|
|
35
37
|
isProxyVariantRequest,
|
|
36
38
|
isSafePath,
|
|
39
|
+
mkdirWithinProject,
|
|
37
40
|
pinWithinProject,
|
|
38
41
|
probeAssetCodec,
|
|
39
42
|
probeMediaMetadata,
|
|
@@ -43,7 +46,7 @@ import {
|
|
|
43
46
|
resolveProxyVariantRequest,
|
|
44
47
|
resolveWithinProject,
|
|
45
48
|
walkDir
|
|
46
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-WQ2WF2YJ.js";
|
|
47
50
|
import {
|
|
48
51
|
STUDIO_MANUAL_EDITS_PATH,
|
|
49
52
|
createStudioManualEditsRenderBodyScript,
|
|
@@ -64,15 +67,7 @@ import { join as join2 } from "path";
|
|
|
64
67
|
// src/helpers/hfIdPersist.ts
|
|
65
68
|
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
66
69
|
import { createHash } from "crypto";
|
|
67
|
-
import {
|
|
68
|
-
closeSync,
|
|
69
|
-
constants,
|
|
70
|
-
fstatSync,
|
|
71
|
-
mkdirSync,
|
|
72
|
-
openSync,
|
|
73
|
-
readFileSync,
|
|
74
|
-
writeFileSync
|
|
75
|
-
} from "fs";
|
|
70
|
+
import { closeSync, constants, fstatSync, openSync, readFileSync, writeFileSync } from "fs";
|
|
76
71
|
import { dirname, join } from "path";
|
|
77
72
|
|
|
78
73
|
// src/helpers/atomicFile.ts
|
|
@@ -123,7 +118,7 @@ function stampProjectHfIds(projectDir) {
|
|
|
123
118
|
next[file] = hash;
|
|
124
119
|
}
|
|
125
120
|
try {
|
|
126
|
-
|
|
121
|
+
mkdirWithinProject(projectDir, dirname(recordPath));
|
|
127
122
|
writeFileSync(recordPath, JSON.stringify(next));
|
|
128
123
|
} catch {
|
|
129
124
|
}
|
|
@@ -408,7 +403,6 @@ import {
|
|
|
408
403
|
readlinkSync,
|
|
409
404
|
writeFileSync as writeFileSync5,
|
|
410
405
|
writeSync,
|
|
411
|
-
mkdirSync as mkdirSync4,
|
|
412
406
|
unlinkSync as unlinkSync2,
|
|
413
407
|
rmSync as rmSync3,
|
|
414
408
|
statSync as statSync3,
|
|
@@ -469,7 +463,7 @@ import { spawn } from "child_process";
|
|
|
469
463
|
import {
|
|
470
464
|
existsSync,
|
|
471
465
|
writeFileSync as writeFileSync2,
|
|
472
|
-
mkdirSync
|
|
466
|
+
mkdirSync,
|
|
473
467
|
lstatSync as lstatSync2,
|
|
474
468
|
mkdtempSync,
|
|
475
469
|
renameSync,
|
|
@@ -558,7 +552,7 @@ function isWaveformCacheDirectory(cacheDir) {
|
|
|
558
552
|
function writeWaveformCache(cachePath, peaks) {
|
|
559
553
|
const cacheDir = dirname2(cachePath);
|
|
560
554
|
try {
|
|
561
|
-
|
|
555
|
+
mkdirSync(cacheDir, { mode: 448 });
|
|
562
556
|
} catch (error) {
|
|
563
557
|
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
564
558
|
}
|
|
@@ -583,27 +577,41 @@ import { spawnSync } from "child_process";
|
|
|
583
577
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
584
578
|
import { tmpdir } from "os";
|
|
585
579
|
import { basename, join as join4 } from "path";
|
|
580
|
+
import { FFPROBE_PATH_ENV, findFfBinary as findFfBinary2 } from "@hyperframes/parsers/ff-binaries";
|
|
586
581
|
var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
|
|
587
|
-
var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
|
|
588
|
-
|
|
582
|
+
var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac|flac)$/i;
|
|
583
|
+
var defaultRunner = spawnSync;
|
|
584
|
+
var FFPROBE_MISSING = "not checked: ffprobe was not found. Install FFmpeg or set HYPERFRAMES_FFPROBE_PATH.";
|
|
585
|
+
function validateUploadedMedia(filePath, runner = defaultRunner) {
|
|
589
586
|
const isVideo = VIDEO_EXT.test(filePath);
|
|
590
587
|
const isAudio = AUDIO_EXT.test(filePath);
|
|
591
588
|
if (!isVideo && !isAudio) {
|
|
592
589
|
return { ok: true };
|
|
593
590
|
}
|
|
591
|
+
const ffprobe = findFfBinary2("ffprobe") ?? (runner === defaultRunner ? void 0 : "ffprobe");
|
|
592
|
+
if (!ffprobe) return { ok: true, unchecked: FFPROBE_MISSING };
|
|
594
593
|
const result = runner(
|
|
595
|
-
|
|
594
|
+
ffprobe,
|
|
596
595
|
["-v", "error", "-show_entries", "stream=codec_type", "-of", "json", "--", filePath],
|
|
597
596
|
{ windowsHide: true }
|
|
598
597
|
);
|
|
598
|
+
if (result.error && process.env[FFPROBE_PATH_ENV]?.trim()) {
|
|
599
|
+
return {
|
|
600
|
+
ok: false,
|
|
601
|
+
reason: `${FFPROBE_PATH_ENV} names "${ffprobe}", which cannot run: fix it or unset it`
|
|
602
|
+
};
|
|
603
|
+
}
|
|
599
604
|
if (result.error?.code === "ENOENT") {
|
|
600
|
-
return { ok: true };
|
|
605
|
+
return { ok: true, unchecked: FFPROBE_MISSING };
|
|
601
606
|
}
|
|
602
607
|
if (result.status !== 0) {
|
|
603
608
|
return { ok: false, reason: "ffprobe failed to read the media file" };
|
|
604
609
|
}
|
|
610
|
+
return checkStreams(String(result.stdout || "{}"), isVideo, isAudio);
|
|
611
|
+
}
|
|
612
|
+
function checkStreams(stdout, isVideo, isAudio) {
|
|
605
613
|
try {
|
|
606
|
-
const parsed = JSON.parse(
|
|
614
|
+
const parsed = JSON.parse(stdout);
|
|
607
615
|
const streams = parsed.streams ?? [];
|
|
608
616
|
const hasVideo = streams.some((stream) => stream.codec_type === "video");
|
|
609
617
|
const hasAudio = streams.some((stream) => stream.codec_type === "audio");
|
|
@@ -618,7 +626,7 @@ function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
|
618
626
|
return { ok: false, reason: "ffprobe returned unreadable media metadata" };
|
|
619
627
|
}
|
|
620
628
|
}
|
|
621
|
-
function validateUploadedMediaBuffer(fileName, buffer, runner =
|
|
629
|
+
function validateUploadedMediaBuffer(fileName, buffer, runner = defaultRunner) {
|
|
622
630
|
const tempDir = mkdtempSync2(join4(tmpdir(), "hyperframes-upload-"));
|
|
623
631
|
const tempPath = join4(tempDir, basename(fileName));
|
|
624
632
|
try {
|
|
@@ -630,7 +638,7 @@ function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
|
|
|
630
638
|
}
|
|
631
639
|
|
|
632
640
|
// src/helpers/backupJournal.ts
|
|
633
|
-
import {
|
|
641
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
634
642
|
import { Buffer as Buffer2 } from "buffer";
|
|
635
643
|
import { join as join5, relative as relative2 } from "path";
|
|
636
644
|
var DEFAULT_KEEP_PER_FILE = 10;
|
|
@@ -652,13 +660,14 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
|
|
|
652
660
|
const content = readFileSync3(absPath);
|
|
653
661
|
const relativePath = relative2(projectDir, absPath);
|
|
654
662
|
const backupDir = join5(projectDir, ".hyperframes", "backup");
|
|
655
|
-
|
|
663
|
+
mkdirWithinProject(projectDir, backupDir);
|
|
656
664
|
const backupKey = backupKeyForPath(relativePath);
|
|
657
665
|
const backupPath = nextBackupPath(backupDir, backupKey);
|
|
658
666
|
writeFileSync4(backupPath, content);
|
|
659
667
|
pruneBackups(backupDir, backupKey, options.keepPerFile ?? DEFAULT_KEEP_PER_FILE);
|
|
660
668
|
return { backupPath };
|
|
661
669
|
} catch (error) {
|
|
670
|
+
if (isProjectRootMissing(error)) throw error;
|
|
662
671
|
if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "EISDIR")) {
|
|
663
672
|
return { backupPath: null };
|
|
664
673
|
}
|
|
@@ -697,6 +706,15 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
|
|
|
697
706
|
}
|
|
698
707
|
}
|
|
699
708
|
|
|
709
|
+
// src/helpers/projectDirMissing.ts
|
|
710
|
+
var PROJECT_DIR_MISSING = { error: "not found", why: "project_dir_missing" };
|
|
711
|
+
var projectDirMissing = (c) => c.json(PROJECT_DIR_MISSING, 404);
|
|
712
|
+
function replaceWithProjectDirMissing(c, hostHeaders) {
|
|
713
|
+
hostHeaders.delete("content-type");
|
|
714
|
+
c.res = void 0;
|
|
715
|
+
c.res = Response.json(PROJECT_DIR_MISSING, { status: 404, headers: hostHeaders });
|
|
716
|
+
}
|
|
717
|
+
|
|
700
718
|
// src/helpers/fileVersion.ts
|
|
701
719
|
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
702
720
|
var RECEIPT_TTL_MS = 1e4;
|
|
@@ -891,6 +909,7 @@ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
|
891
909
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
892
910
|
import { dirname as dirname3, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
|
|
893
911
|
import { parseHTML } from "linkedom";
|
|
912
|
+
import { ProjectRootMissingError } from "@hyperframes/core";
|
|
894
913
|
var CompositionInsertionError = class extends Error {
|
|
895
914
|
constructor(message, status) {
|
|
896
915
|
super(message);
|
|
@@ -920,6 +939,7 @@ function positiveAttribute(root, ...names) {
|
|
|
920
939
|
}
|
|
921
940
|
function canonicalProjectPath(projectDir, candidate) {
|
|
922
941
|
if (!candidate) {
|
|
942
|
+
if (folderGone(projectDir)) throw new ProjectRootMissingError(projectDir);
|
|
923
943
|
throw new CompositionInsertionError("Composition source escapes the project", 400);
|
|
924
944
|
}
|
|
925
945
|
if (!existsSync2(candidate)) {
|
|
@@ -1172,10 +1192,8 @@ async function resolveProjectPath(c, adapter, route, opts) {
|
|
|
1172
1192
|
if (!project) {
|
|
1173
1193
|
return { error: c.json({ error: "not found" }, 404) };
|
|
1174
1194
|
}
|
|
1175
|
-
if (
|
|
1176
|
-
return {
|
|
1177
|
-
error: c.json({ error: "not found", why: "project_dir_missing" }, 404)
|
|
1178
|
-
};
|
|
1195
|
+
if (folderGone(project.dir)) {
|
|
1196
|
+
return { error: projectDirMissing(c) };
|
|
1179
1197
|
}
|
|
1180
1198
|
const filePath = requestSubPath(c.req.url, `projects/:id/${route}`);
|
|
1181
1199
|
if (filePath.includes("\0")) {
|
|
@@ -1379,9 +1397,8 @@ async function parseMutationBody(c) {
|
|
|
1379
1397
|
}
|
|
1380
1398
|
return { target: body.target, body };
|
|
1381
1399
|
}
|
|
1382
|
-
function ensureDir(filePath) {
|
|
1383
|
-
|
|
1384
|
-
if (!existsSync3(dir)) mkdirSync4(dir, { recursive: true });
|
|
1400
|
+
function ensureDir(projectDir, filePath) {
|
|
1401
|
+
mkdirWithinProject(projectDir, dirname4(filePath));
|
|
1385
1402
|
}
|
|
1386
1403
|
function generateCopyPath(projectDir, originalPath) {
|
|
1387
1404
|
const ext = originalPath.includes(".") ? "." + originalPath.split(".").pop() : "";
|
|
@@ -2502,6 +2519,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2502
2519
|
const uploaded = [];
|
|
2503
2520
|
const skipped = [];
|
|
2504
2521
|
const invalid = [];
|
|
2522
|
+
const unchecked = [];
|
|
2505
2523
|
const entries = formData.entries();
|
|
2506
2524
|
const subDir = targetDir === projectDir ? "" : targetDir.slice(projectDir.length + 1);
|
|
2507
2525
|
for (const [, value] of entries) {
|
|
@@ -2564,12 +2582,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2564
2582
|
}
|
|
2565
2583
|
const relativePath = subDir ? join6(subDir, finalName) : finalName;
|
|
2566
2584
|
uploaded.push(relativePath);
|
|
2585
|
+
if (validation.unchecked) unchecked.push({ name: finalName, reason: validation.unchecked });
|
|
2567
2586
|
if (isAudioFile(finalName)) {
|
|
2568
2587
|
generateWaveformCache(projectDir, relativePath).catch(() => {
|
|
2569
2588
|
});
|
|
2570
2589
|
}
|
|
2571
2590
|
}
|
|
2572
|
-
return { uploaded, skipped, invalid };
|
|
2591
|
+
return { uploaded, skipped, invalid, unchecked };
|
|
2573
2592
|
}
|
|
2574
2593
|
function registerFileRoutes(api, adapter) {
|
|
2575
2594
|
api.get("/projects/:id/files/*", async (c) => {
|
|
@@ -2629,7 +2648,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2629
2648
|
let backup = { backupPath: null };
|
|
2630
2649
|
let overwrote;
|
|
2631
2650
|
if (createOnly) {
|
|
2632
|
-
ensureDir(res.absPath);
|
|
2651
|
+
ensureDir(res.project.dir, res.absPath);
|
|
2633
2652
|
let fd;
|
|
2634
2653
|
try {
|
|
2635
2654
|
fd = openSync2(res.absPath, "wx");
|
|
@@ -2711,7 +2730,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2711
2730
|
api.post("/projects/:id/files/*", async (c) => {
|
|
2712
2731
|
const res = await resolveProjectFile(c, adapter);
|
|
2713
2732
|
if ("error" in res) return res.error;
|
|
2714
|
-
ensureDir(res.absPath);
|
|
2733
|
+
ensureDir(res.project.dir, res.absPath);
|
|
2715
2734
|
const body = Buffer.from(await c.req.arrayBuffer());
|
|
2716
2735
|
try {
|
|
2717
2736
|
writeFileSync5(res.absPath, body, { flag: "wx" });
|
|
@@ -3252,7 +3271,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3252
3271
|
if (existsSync3(newAbs)) {
|
|
3253
3272
|
return c.json({ error: "already exists" }, 409);
|
|
3254
3273
|
}
|
|
3255
|
-
ensureDir(newAbs);
|
|
3274
|
+
ensureDir(res.project.dir, newAbs);
|
|
3256
3275
|
renameSync2(res.absPath, newAbs);
|
|
3257
3276
|
const updatedFiles = updateReferences(res.project.dir, res.filePath, body.newPath);
|
|
3258
3277
|
return c.json({ ok: true, path: body.newPath, updatedReferences: updatedFiles });
|
|
@@ -3273,7 +3292,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3273
3292
|
if (!destAbs) {
|
|
3274
3293
|
return c.json({ error: "forbidden" }, 403);
|
|
3275
3294
|
}
|
|
3276
|
-
ensureDir(destAbs);
|
|
3295
|
+
ensureDir(project.dir, destAbs);
|
|
3277
3296
|
try {
|
|
3278
3297
|
writeFileSync5(destAbs, readFileSync6(srcAbs), { flag: "wx" });
|
|
3279
3298
|
} catch (error) {
|
|
@@ -3294,14 +3313,22 @@ function registerFileRoutes(api, adapter) {
|
|
|
3294
3313
|
async (c) => {
|
|
3295
3314
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
3296
3315
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
3316
|
+
if (folderGone(project.dir)) return projectDirMissing(c);
|
|
3297
3317
|
const subDir = c.req.query("dir") ?? "";
|
|
3298
3318
|
const targetDir = subDir ? resolveWithinProject(project.dir, subDir) : project.dir;
|
|
3299
3319
|
if (!targetDir) return c.json({ error: "forbidden" }, 403);
|
|
3300
|
-
if (subDir && !existsSync3(targetDir)) mkdirSync4(targetDir, { recursive: true });
|
|
3301
3320
|
const formData = await c.req.formData();
|
|
3321
|
+
mkdirWithinProject(project.dir, targetDir);
|
|
3302
3322
|
const result = await processUploadedFiles(formData, targetDir, project.dir);
|
|
3323
|
+
if (folderGone(project.dir)) return projectDirMissing(c);
|
|
3303
3324
|
return c.json(
|
|
3304
|
-
{
|
|
3325
|
+
{
|
|
3326
|
+
ok: true,
|
|
3327
|
+
files: result.uploaded,
|
|
3328
|
+
skipped: result.skipped,
|
|
3329
|
+
invalid: result.invalid,
|
|
3330
|
+
unchecked: result.unchecked
|
|
3331
|
+
},
|
|
3305
3332
|
201
|
|
3306
3333
|
);
|
|
3307
3334
|
}
|
|
@@ -3387,6 +3414,8 @@ import {
|
|
|
3387
3414
|
insertBeforeCloseTag as insertBeforeCloseTag2,
|
|
3388
3415
|
stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2
|
|
3389
3416
|
} from "@hyperframes/core/compiler";
|
|
3417
|
+
import { STUDIO_PREVIEW_MARK_META } from "@hyperframes/core/studio-preview-mark";
|
|
3418
|
+
import { injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
|
|
3390
3419
|
import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
3391
3420
|
|
|
3392
3421
|
// src/helpers/subComposition.ts
|
|
@@ -3671,6 +3700,34 @@ function injectPreviewVariables(html, values) {
|
|
|
3671
3700
|
return tag + html;
|
|
3672
3701
|
}
|
|
3673
3702
|
|
|
3703
|
+
// src/helpers/lazyPreviewImages.ts
|
|
3704
|
+
import { findStartTags } from "@hyperframes/core/compiler/html-document";
|
|
3705
|
+
import { parseHTML as parseHTML4 } from "linkedom";
|
|
3706
|
+
import { STUDIO_PREVIEW_LAZY_ATTR } from "@hyperframes/core/studio-preview-mark";
|
|
3707
|
+
function startsAfterZero(el) {
|
|
3708
|
+
for (let node = el; node; node = node.parentElement) {
|
|
3709
|
+
if (Number(node.getAttribute("data-start")) > 0) return true;
|
|
3710
|
+
}
|
|
3711
|
+
return false;
|
|
3712
|
+
}
|
|
3713
|
+
var hasLoading = (img) => Array.from(img.attributes).some((attr) => attr.name.toLowerCase() === "loading");
|
|
3714
|
+
function lazyPreviewImages(html) {
|
|
3715
|
+
if (!/<!doctype|<html[\s>]/i.test(html)) return html;
|
|
3716
|
+
const images = [...parseHTML4(html).document.querySelectorAll("img")];
|
|
3717
|
+
const tags = findStartTags(html, "img");
|
|
3718
|
+
if (tags.length !== images.length) return html;
|
|
3719
|
+
const parts = [];
|
|
3720
|
+
let from = 0;
|
|
3721
|
+
images.forEach((img, i) => {
|
|
3722
|
+
if (hasLoading(img) || !startsAfterZero(img)) return;
|
|
3723
|
+
const at = (tags[i] ?? 0) + 4;
|
|
3724
|
+
parts.push(html.slice(from, at), ` loading="lazy" ${STUDIO_PREVIEW_LAZY_ATTR}`);
|
|
3725
|
+
from = at;
|
|
3726
|
+
});
|
|
3727
|
+
parts.push(html.slice(from));
|
|
3728
|
+
return parts.join("");
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3674
3731
|
// src/routes/preview.ts
|
|
3675
3732
|
var PROJECT_SIGNATURE_META = "hyperframes-project-signature";
|
|
3676
3733
|
var GSAP_CDN_VERSION = "3.15.0";
|
|
@@ -3763,7 +3820,8 @@ function injectStudioMotionScript(html, projectDir, activeCompositionPath) {
|
|
|
3763
3820
|
false
|
|
3764
3821
|
);
|
|
3765
3822
|
}
|
|
3766
|
-
var
|
|
3823
|
+
var GSAP_FALLBACK_ATTR = "data-hf-gsap-fallback";
|
|
3824
|
+
var GSAP_CDN_FALLBACK_SCRIPT = `<script ${GSAP_FALLBACK_ATTR}>
|
|
3767
3825
|
(function(){
|
|
3768
3826
|
var cdnBase="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/";
|
|
3769
3827
|
var loaded={};
|
|
@@ -3784,7 +3842,7 @@ var GSAP_CDN_FALLBACK_SCRIPT = `<script data-hf-gsap-fallback>
|
|
|
3784
3842
|
})();
|
|
3785
3843
|
</script>`;
|
|
3786
3844
|
function injectGsapCdnFallback(html) {
|
|
3787
|
-
if (html.includes(
|
|
3845
|
+
if (html.includes(GSAP_FALLBACK_ATTR)) return html;
|
|
3788
3846
|
if (html.includes("<head>")) return html.replace("<head>", "<head>" + GSAP_CDN_FALLBACK_SCRIPT);
|
|
3789
3847
|
return GSAP_CDN_FALLBACK_SCRIPT + html;
|
|
3790
3848
|
}
|
|
@@ -3810,11 +3868,13 @@ function previewVariablesFromRequest(rawVariables) {
|
|
|
3810
3868
|
if (!parse2.ok) return { error: parse2.error };
|
|
3811
3869
|
return { raw: rawVariables, values: parse2.values };
|
|
3812
3870
|
}
|
|
3813
|
-
|
|
3871
|
+
var PREVIEW_CAPTURE_PARAM = "hf-capture";
|
|
3872
|
+
function injectStudioPreviewAugmentations(html, adapter, projectDir, activeCompositionPath, capture) {
|
|
3873
|
+
const marked = capture ? html : injectTagsAtHeadStart(lazyPreviewImages(html), `<meta name="${STUDIO_PREVIEW_MARK_META}">`);
|
|
3814
3874
|
return injectStudioMotionScript(
|
|
3815
3875
|
injectMotionPathPluginIfNeeded(
|
|
3816
3876
|
injectGsapCdnFallback(
|
|
3817
|
-
injectProjectSignature(
|
|
3877
|
+
injectProjectSignature(marked, resolveProjectSignature(adapter, projectDir))
|
|
3818
3878
|
)
|
|
3819
3879
|
),
|
|
3820
3880
|
projectDir,
|
|
@@ -3872,7 +3932,7 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3872
3932
|
if (builtPreviews.size > 4) builtPreviews.delete(builtPreviews.keys().next().value);
|
|
3873
3933
|
};
|
|
3874
3934
|
const previewBuilds = /* @__PURE__ */ new Map();
|
|
3875
|
-
async function buildPreview(project, previewVariables, builtKey) {
|
|
3935
|
+
async function buildPreview(project, previewVariables, builtKey, capture) {
|
|
3876
3936
|
const diskMain = resolveProjectMainHtml(project.dir, project.id);
|
|
3877
3937
|
const normalizedDisk = diskMain ? ensureHfIds3(diskMain.html) : null;
|
|
3878
3938
|
try {
|
|
@@ -3902,7 +3962,8 @@ ${runtimeTag}`;
|
|
|
3902
3962
|
ensureHfIds3(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
|
|
3903
3963
|
adapter,
|
|
3904
3964
|
project.dir,
|
|
3905
|
-
mainCompositionPath
|
|
3965
|
+
mainCompositionPath,
|
|
3966
|
+
capture
|
|
3906
3967
|
);
|
|
3907
3968
|
if (previewVariables) bundled = injectPreviewVariables(bundled, previewVariables);
|
|
3908
3969
|
bundled = await injectMediaCodecMap(
|
|
@@ -3914,7 +3975,7 @@ ${runtimeTag}`;
|
|
|
3914
3975
|
);
|
|
3915
3976
|
bundled = addScenePartsManifest(bundled, [`meta[name="${PROJECT_SIGNATURE_META}"]`]);
|
|
3916
3977
|
rememberPreview(builtKey, bundled);
|
|
3917
|
-
adapter.previewDocuments?.write(builtKey, bundled);
|
|
3978
|
+
if (!capture) adapter.previewDocuments?.write(builtKey, bundled);
|
|
3918
3979
|
return bundled;
|
|
3919
3980
|
} catch {
|
|
3920
3981
|
const fallback = resolveProjectMainHtml(project.dir, project.id);
|
|
@@ -3924,7 +3985,8 @@ ${runtimeTag}`;
|
|
|
3924
3985
|
await transformPreviewHtml(fallbackHtml, adapter, project, fallback.compositionPath),
|
|
3925
3986
|
adapter,
|
|
3926
3987
|
project.dir,
|
|
3927
|
-
fallback.compositionPath
|
|
3988
|
+
fallback.compositionPath,
|
|
3989
|
+
capture
|
|
3928
3990
|
);
|
|
3929
3991
|
if (previewVariables) {
|
|
3930
3992
|
fallbackAugmented = injectPreviewVariables(fallbackAugmented, previewVariables);
|
|
@@ -3948,7 +4010,8 @@ ${runtimeTag}`;
|
|
|
3948
4010
|
const vars = previewVariablesFromRequest(c.req.query("variables"));
|
|
3949
4011
|
if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
|
|
3950
4012
|
const previewVariables = vars.values;
|
|
3951
|
-
const
|
|
4013
|
+
const capture = c.req.query(PREVIEW_CAPTURE_PARAM) !== void 0;
|
|
4014
|
+
const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}${capture ? ":capture" : ""}"`;
|
|
3952
4015
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3953
4016
|
if (ifNoneMatch === etag) {
|
|
3954
4017
|
return new Response(null, {
|
|
@@ -3965,7 +4028,7 @@ ${etag}`;
|
|
|
3965
4028
|
}
|
|
3966
4029
|
let pending = previewBuilds.get(builtKey);
|
|
3967
4030
|
if (!pending) {
|
|
3968
|
-
pending = buildPreview(project, previewVariables, builtKey).finally(
|
|
4031
|
+
pending = buildPreview(project, previewVariables, builtKey, capture).finally(
|
|
3969
4032
|
() => previewBuilds.delete(builtKey)
|
|
3970
4033
|
);
|
|
3971
4034
|
previewBuilds.set(builtKey, pending);
|
|
@@ -3997,7 +4060,8 @@ ${etag}`;
|
|
|
3997
4060
|
return c.text("not found", 404);
|
|
3998
4061
|
}
|
|
3999
4062
|
const compPathHash = createHash4("sha1").update(compPath).digest("hex");
|
|
4000
|
-
const
|
|
4063
|
+
const capture = c.req.query(PREVIEW_CAPTURE_PARAM) !== void 0;
|
|
4064
|
+
const etag = `"comp:v2:${compPathHash}:${signature}${variablesEtagSalt(vars.raw)}${capture ? ":capture" : ""}"`;
|
|
4001
4065
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
4002
4066
|
if (ifNoneMatch === etag) {
|
|
4003
4067
|
return new Response(null, {
|
|
@@ -4018,7 +4082,7 @@ ${etag}`;
|
|
|
4018
4082
|
if (!html) return c.text("not found", 404);
|
|
4019
4083
|
recordPreviewReferences(project.dir, html);
|
|
4020
4084
|
html = ensureHfIds3(await transformPreviewHtml(html, adapter, project, compPath));
|
|
4021
|
-
html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath);
|
|
4085
|
+
html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath, capture);
|
|
4022
4086
|
if (previewVariables) html = injectPreviewVariables(html, previewVariables);
|
|
4023
4087
|
html = await injectMediaCodecMap(html, adapter, project.dir, compPath, mediaCodecProbeCache);
|
|
4024
4088
|
return c.html(html, 200, previewCacheHeaders(etag));
|
|
@@ -4075,6 +4139,7 @@ ${etag}`;
|
|
|
4075
4139
|
if (err instanceof ProxyCapacityError) {
|
|
4076
4140
|
return c.text(err.message, 503, { "Retry-After": "5" });
|
|
4077
4141
|
}
|
|
4142
|
+
if (isProjectRootMissing(err)) throw err;
|
|
4078
4143
|
const message = err instanceof ProxyTranscodeError ? err.message : "proxy transcode failed";
|
|
4079
4144
|
return c.text(message, 502);
|
|
4080
4145
|
}
|
|
@@ -4176,7 +4241,7 @@ function registerLintRoutes(api, adapter) {
|
|
|
4176
4241
|
|
|
4177
4242
|
// src/routes/render.ts
|
|
4178
4243
|
import { streamSSE } from "hono/streaming";
|
|
4179
|
-
import { existsSync as existsSync6, readFileSync as readFileSync10,
|
|
4244
|
+
import { existsSync as existsSync6, readFileSync as readFileSync10, unlinkSync as unlinkSync3, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
4180
4245
|
import { join as join11 } from "path";
|
|
4181
4246
|
import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
|
|
4182
4247
|
import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
|
|
@@ -4229,6 +4294,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4229
4294
|
let composition;
|
|
4230
4295
|
if (typeof body.composition === "string" && body.composition.length > 0) {
|
|
4231
4296
|
if (!resolveWithinProject(project.dir, body.composition)) {
|
|
4297
|
+
if (folderGone(project.dir)) return projectDirMissing(c);
|
|
4232
4298
|
return c.json({ error: "composition path must be within the project directory" }, 400);
|
|
4233
4299
|
}
|
|
4234
4300
|
composition = body.composition;
|
|
@@ -4243,7 +4309,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4243
4309
|
const now = /* @__PURE__ */ new Date();
|
|
4244
4310
|
const jobId = `${project.id}_${formatRenderOutputTimestamp(now)}`;
|
|
4245
4311
|
const rendersDir = adapter.rendersDir(project);
|
|
4246
|
-
|
|
4312
|
+
mkdirWithinProject(project.dir, rendersDir);
|
|
4247
4313
|
const ext = FORMAT_EXT[format] ?? ".mp4";
|
|
4248
4314
|
const outputPath = join11(rendersDir, `${jobId}${ext}`);
|
|
4249
4315
|
const jobState = adapter.startRender({
|
|
@@ -4624,7 +4690,6 @@ import {
|
|
|
4624
4690
|
closeSync as closeSync3,
|
|
4625
4691
|
fstatSync as fstatSync3,
|
|
4626
4692
|
openSync as openSync3,
|
|
4627
|
-
mkdirSync as mkdirSync6,
|
|
4628
4693
|
readFileSync as readFileSync12,
|
|
4629
4694
|
readdirSync as readdirSync5,
|
|
4630
4695
|
renameSync as renameSync3,
|
|
@@ -4817,7 +4882,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4817
4882
|
const motion = manifestKey(join12(project.dir, STUDIO_MOTION_PATH));
|
|
4818
4883
|
sourceMtime = Math.max(sourceMtime, manualEdits.mtimeMs, motion.mtimeMs);
|
|
4819
4884
|
const projectUrl = `http://${c.req.header("host")}/api/projects/${encodeURIComponent(project.id)}`;
|
|
4820
|
-
const
|
|
4885
|
+
const previewPath = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
|
|
4886
|
+
const previewUrl = `${previewPath}?${PREVIEW_CAPTURE_PARAM}=1`;
|
|
4821
4887
|
const cacheDir = join12(project.dir, ".thumbnails");
|
|
4822
4888
|
const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
|
|
4823
4889
|
const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
|
|
@@ -4840,7 +4906,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4840
4906
|
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
4841
4907
|
});
|
|
4842
4908
|
}
|
|
4843
|
-
if (url.searchParams.get("cached") === "1")
|
|
4909
|
+
if (url.searchParams.get("cached") === "1")
|
|
4910
|
+
return c.body(null, 204, { "Cache-Control": "no-cache" });
|
|
4844
4911
|
try {
|
|
4845
4912
|
const buffer = await thumbnailGenerationCoordinator.acquire(
|
|
4846
4913
|
cachePath,
|
|
@@ -4866,7 +4933,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4866
4933
|
if (afterGeneration?.project.dir !== project.dir || !inputsUnchanged(afterGeneration.signature) || !inputsUnchanged(createProjectSignature(project.dir))) {
|
|
4867
4934
|
return generated;
|
|
4868
4935
|
}
|
|
4869
|
-
|
|
4936
|
+
mkdirWithinProject(project.dir, cacheDir);
|
|
4870
4937
|
writeThumbnailAtomically(cachePath, generated);
|
|
4871
4938
|
return generated;
|
|
4872
4939
|
}
|
|
@@ -4885,6 +4952,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4885
4952
|
if (err instanceof DOMException && err.name === "AbortError") {
|
|
4886
4953
|
return new Response(null, { status: 499 });
|
|
4887
4954
|
}
|
|
4955
|
+
if (isProjectRootMissing(err)) throw err;
|
|
4888
4956
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4889
4957
|
return c.json({ error: `Thumbnail generation failed: ${msg}` }, 500);
|
|
4890
4958
|
}
|
|
@@ -5079,6 +5147,7 @@ function registerRegistryRoutes(api, adapter) {
|
|
|
5079
5147
|
}
|
|
5080
5148
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
5081
5149
|
if (!project) return c.json({ error: "Project not found" }, 404);
|
|
5150
|
+
if (folderGone(project.dir)) return projectDirMissing(c);
|
|
5082
5151
|
const body = await c.req.json().catch(() => null);
|
|
5083
5152
|
if (!body?.blockName) {
|
|
5084
5153
|
return c.json({ error: "blockName is required" }, 400);
|
|
@@ -5087,6 +5156,7 @@ function registerRegistryRoutes(api, adapter) {
|
|
|
5087
5156
|
const result = await adapter.installRegistryBlock({ project, blockName: body.blockName });
|
|
5088
5157
|
return c.json(result);
|
|
5089
5158
|
} catch (err) {
|
|
5159
|
+
if (isProjectRootMissing(err)) return projectDirMissing(c);
|
|
5090
5160
|
const message = err instanceof Error ? err.message : "Install failed";
|
|
5091
5161
|
return c.json({ error: message }, 500);
|
|
5092
5162
|
}
|
|
@@ -5193,8 +5263,8 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
5193
5263
|
|
|
5194
5264
|
// src/routes/media.ts
|
|
5195
5265
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
5196
|
-
import { existsSync as existsSync8
|
|
5197
|
-
import { basename as basename2, dirname as dirname6, extname as extname2, join as join14 } from "path";
|
|
5266
|
+
import { existsSync as existsSync8 } from "fs";
|
|
5267
|
+
import { basename as basename2, dirname as dirname6, extname as extname2, join as join14, posix } from "path";
|
|
5198
5268
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5199
5269
|
".mp4",
|
|
5200
5270
|
".mov",
|
|
@@ -5227,21 +5297,31 @@ function slugFileBase(path) {
|
|
|
5227
5297
|
const name = basename2(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5228
5298
|
return name || "media";
|
|
5229
5299
|
}
|
|
5230
|
-
function uniqueAssetPath(projectDir, assetPath) {
|
|
5300
|
+
function uniqueAssetPath(projectDir, assetPath, taken) {
|
|
5231
5301
|
const ext = extname2(assetPath);
|
|
5232
5302
|
const withoutExt = assetPath.slice(0, -ext.length);
|
|
5233
5303
|
let candidate = assetPath;
|
|
5234
|
-
for (let index = 2; existsSync8(join14(projectDir, candidate)); index++) {
|
|
5304
|
+
for (let index = 2; taken.has(candidate) || existsSync8(join14(projectDir, candidate)); index++) {
|
|
5235
5305
|
candidate = `${withoutExt}-${index}${ext}`;
|
|
5236
5306
|
}
|
|
5237
5307
|
return candidate;
|
|
5238
5308
|
}
|
|
5239
|
-
function defaultOutputPath(projectDir, inputPath) {
|
|
5309
|
+
function defaultOutputPath(projectDir, inputPath, taken) {
|
|
5240
5310
|
const ext = isImagePath(inputPath) ? ".png" : ".webm";
|
|
5241
|
-
return uniqueAssetPath(
|
|
5311
|
+
return uniqueAssetPath(
|
|
5312
|
+
projectDir,
|
|
5313
|
+
`assets/cutouts/${slugFileBase(inputPath)}-cutout${ext}`,
|
|
5314
|
+
taken
|
|
5315
|
+
);
|
|
5316
|
+
}
|
|
5317
|
+
function defaultPlatePath(projectDir, inputPath, taken) {
|
|
5318
|
+
return uniqueAssetPath(projectDir, `assets/cutouts/${slugFileBase(inputPath)}-plate.webm`, taken);
|
|
5242
5319
|
}
|
|
5243
|
-
function
|
|
5244
|
-
|
|
5320
|
+
function outputsInFlight(mediaJobs) {
|
|
5321
|
+
const running = [...mediaJobs.values()].filter((job) => job.status === "processing");
|
|
5322
|
+
return new Set(
|
|
5323
|
+
running.flatMap((job) => [job.outputAssetPath, job.backgroundOutputAssetPath ?? ""])
|
|
5324
|
+
);
|
|
5245
5325
|
}
|
|
5246
5326
|
function makeJobId(projectId, mediaJobs) {
|
|
5247
5327
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
@@ -5310,15 +5390,16 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5310
5390
|
if (!inputIsVideo && !inputIsImage) {
|
|
5311
5391
|
return c.json({ error: "background removal supports video or image assets only" }, 400);
|
|
5312
5392
|
}
|
|
5313
|
-
const requestedOutput = body.outputPath ? normalizeProjectAssetPath(body.outputPath) : "";
|
|
5393
|
+
const requestedOutput = body.outputPath ? posix.normalize(normalizeProjectAssetPath(body.outputPath)) : "";
|
|
5314
5394
|
if (requestedOutput && containsNullByte(requestedOutput)) {
|
|
5315
5395
|
return c.json({ error: "forbidden" }, 403);
|
|
5316
5396
|
}
|
|
5317
5397
|
if (requestedOutput && !resolveWithinProject(project.dir, requestedOutput)) {
|
|
5318
5398
|
return c.json({ error: "forbidden" }, 403);
|
|
5319
5399
|
}
|
|
5320
|
-
const
|
|
5321
|
-
const
|
|
5400
|
+
const taken = outputsInFlight(mediaJobs);
|
|
5401
|
+
const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput, taken) : defaultOutputPath(project.dir, inputAssetPath, taken);
|
|
5402
|
+
const outputPath = pinWithinProject(project.dir, outputAssetPath);
|
|
5322
5403
|
if (!outputPath) return c.json({ error: "forbidden" }, 403);
|
|
5323
5404
|
if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname2(outputAssetPath).toLowerCase())) {
|
|
5324
5405
|
return c.json({ error: "video background removal output must be .webm or .mov" }, 400);
|
|
@@ -5332,14 +5413,15 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5332
5413
|
if (!inputIsVideo) {
|
|
5333
5414
|
return c.json({ error: "background plates are only supported for video inputs" }, 400);
|
|
5334
5415
|
}
|
|
5335
|
-
|
|
5336
|
-
|
|
5416
|
+
taken.add(outputAssetPath);
|
|
5417
|
+
backgroundOutputAssetPath = defaultPlatePath(project.dir, inputAssetPath, taken);
|
|
5418
|
+
backgroundOutputPath = pinWithinProject(project.dir, backgroundOutputAssetPath) ?? void 0;
|
|
5337
5419
|
if (!backgroundOutputPath) {
|
|
5338
5420
|
return c.json({ error: "forbidden" }, 403);
|
|
5339
5421
|
}
|
|
5340
5422
|
}
|
|
5341
|
-
|
|
5342
|
-
if (backgroundOutputPath)
|
|
5423
|
+
mkdirWithinProject(project.dir, dirname6(outputPath));
|
|
5424
|
+
if (backgroundOutputPath) mkdirWithinProject(project.dir, dirname6(backgroundOutputPath));
|
|
5343
5425
|
const jobId = makeJobId(project.id, mediaJobs);
|
|
5344
5426
|
const state = adapter.startBackgroundRemoval({
|
|
5345
5427
|
project,
|
|
@@ -5421,20 +5503,20 @@ function registerGlobalAssetRoutes(api) {
|
|
|
5421
5503
|
}
|
|
5422
5504
|
|
|
5423
5505
|
// src/history/projectHistory.ts
|
|
5424
|
-
import { randomUUID as
|
|
5425
|
-
import { readdir as
|
|
5506
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
5507
|
+
import { readdir as readdir3, rm as rm3, rmdir, writeFile } from "fs/promises";
|
|
5426
5508
|
import {
|
|
5427
5509
|
existsSync as existsSync11,
|
|
5428
5510
|
lstatSync as lstatSync4,
|
|
5429
|
-
statSync as
|
|
5430
|
-
mkdirSync as
|
|
5431
|
-
readdirSync as
|
|
5432
|
-
readFileSync as
|
|
5511
|
+
statSync as statSync10,
|
|
5512
|
+
mkdirSync as mkdirSync5,
|
|
5513
|
+
readdirSync as readdirSync7,
|
|
5514
|
+
readFileSync as readFileSync19,
|
|
5433
5515
|
realpathSync,
|
|
5434
5516
|
rmSync as rmSync6
|
|
5435
5517
|
} from "fs";
|
|
5436
5518
|
import { homedir as homedir2 } from "os";
|
|
5437
|
-
import { dirname as
|
|
5519
|
+
import { dirname as dirname10, isAbsolute as isAbsolute2, join as join20, relative as relative5, resolve as resolve8, sep as sep3 } from "path";
|
|
5438
5520
|
|
|
5439
5521
|
// src/history/blobStore.ts
|
|
5440
5522
|
import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
|
|
@@ -5503,12 +5585,20 @@ async function openBlobStore(dir) {
|
|
|
5503
5585
|
};
|
|
5504
5586
|
}
|
|
5505
5587
|
|
|
5588
|
+
// src/history/pruneHistories.ts
|
|
5589
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
5590
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync17, renameSync as renameSync5, statSync as statSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
5591
|
+
import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
|
|
5592
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
5593
|
+
import { dirname as dirname8, join as join19 } from "path";
|
|
5594
|
+
|
|
5506
5595
|
// src/history/historyId.ts
|
|
5507
5596
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
5508
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
5597
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync2, readFileSync as readFileSync15, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
5509
5598
|
import { basename as basename3, join as join17, resolve as resolve7 } from "path";
|
|
5510
5599
|
var ID_PATH = join17(".hyperframes", "history-id");
|
|
5511
5600
|
var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
5601
|
+
var isHistoryId = (name) => ID_SHAPE.test(name);
|
|
5512
5602
|
function readId(projectDir) {
|
|
5513
5603
|
try {
|
|
5514
5604
|
const id = readFileSync15(join17(projectDir, ID_PATH), "utf-8").trim();
|
|
@@ -5552,7 +5642,7 @@ function projectHistoryId(projectDir, historyRoot) {
|
|
|
5552
5642
|
);
|
|
5553
5643
|
if (!id || existsSync10(join17(historyRoot, id)) && !isRecordedFolder(join17(historyRoot, id), folder)) {
|
|
5554
5644
|
id = randomUUID6();
|
|
5555
|
-
|
|
5645
|
+
mkdirWithinProject(dir, join17(dir, ".hyperframes"));
|
|
5556
5646
|
writeFileSync7(join17(dir, ID_PATH), `${id}
|
|
5557
5647
|
`);
|
|
5558
5648
|
}
|
|
@@ -5566,14 +5656,14 @@ var HistoryIdError = class extends Error {
|
|
|
5566
5656
|
}
|
|
5567
5657
|
};
|
|
5568
5658
|
function recordProject(historyDir, dir, folder) {
|
|
5569
|
-
const record = { dir, ino: folder.ino, born: folder.birthtimeMs };
|
|
5570
|
-
|
|
5659
|
+
const record = { dir, ino: folder.ino, born: folder.birthtimeMs, dev: folder.dev };
|
|
5660
|
+
mkdirSync2(historyDir, { recursive: true });
|
|
5571
5661
|
replaceFileAtomically(join17(historyDir, "project.json"), JSON.stringify(record), 420);
|
|
5572
5662
|
}
|
|
5573
5663
|
|
|
5574
5664
|
// src/history/ownerLock.ts
|
|
5575
5665
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
5576
|
-
import { linkSync, mkdirSync as
|
|
5666
|
+
import { linkSync, mkdirSync as mkdirSync3, readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
5577
5667
|
import { join as join18 } from "path";
|
|
5578
5668
|
var HistoryBusyError = class extends Error {
|
|
5579
5669
|
constructor(pid) {
|
|
@@ -5635,7 +5725,7 @@ function evictDeadOwner(file) {
|
|
|
5635
5725
|
async function takeHistoryOwnership(home, waitMs) {
|
|
5636
5726
|
const file = join18(home, "owner.pid");
|
|
5637
5727
|
const deadline = Date.now() + waitMs;
|
|
5638
|
-
|
|
5728
|
+
mkdirSync3(home, { recursive: true });
|
|
5639
5729
|
for (; ; ) {
|
|
5640
5730
|
if (claim(file)) {
|
|
5641
5731
|
let held = true;
|
|
@@ -5652,9 +5742,155 @@ async function takeHistoryOwnership(home, waitMs) {
|
|
|
5652
5742
|
}
|
|
5653
5743
|
}
|
|
5654
5744
|
|
|
5745
|
+
// src/history/pruneHistories.ts
|
|
5746
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
5747
|
+
var KEEP_GONE_PROJECT_HISTORY_MS = 14 * DAY_MS;
|
|
5748
|
+
var PRUNED_PREFIX = ".pruned-";
|
|
5749
|
+
var LAST_PRUNE_FILE = ".last-prune";
|
|
5750
|
+
function listProjectHistories(historyRoot) {
|
|
5751
|
+
return namesIn(historyRoot).filter(isHistoryId).flatMap((id) => {
|
|
5752
|
+
const home = join19(historyRoot, id);
|
|
5753
|
+
const projectDir = readRecord(home)?.dir;
|
|
5754
|
+
if (typeof projectDir !== "string" || readId(projectDir) !== id) return [];
|
|
5755
|
+
return [{ id, projectDir, lastUsedMs: lastUsed(home) }];
|
|
5756
|
+
});
|
|
5757
|
+
}
|
|
5758
|
+
function namesIn(historyRoot) {
|
|
5759
|
+
try {
|
|
5760
|
+
return readdirSync6(historyRoot);
|
|
5761
|
+
} catch (error) {
|
|
5762
|
+
if (error.code === "ENOENT") return [];
|
|
5763
|
+
throw error;
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
async function pruneGoneProjectHistories(historyRoot, options = {}) {
|
|
5767
|
+
return (await pruneWithin(historyRoot, options)).pruned;
|
|
5768
|
+
}
|
|
5769
|
+
async function pruneWithin(historyRoot, {
|
|
5770
|
+
now = Date.now(),
|
|
5771
|
+
dryRun = false,
|
|
5772
|
+
tempDir = tmpdir2(),
|
|
5773
|
+
onError = (error) => console.warn("[history] could not prune a history:", error),
|
|
5774
|
+
budgetMs = Infinity,
|
|
5775
|
+
measure = true
|
|
5776
|
+
}) {
|
|
5777
|
+
const started = performance.now();
|
|
5778
|
+
const names = namesIn(historyRoot);
|
|
5779
|
+
const pruned = [];
|
|
5780
|
+
for (const name of names) {
|
|
5781
|
+
if (performance.now() - started >= budgetMs) return { pruned, finished: false };
|
|
5782
|
+
try {
|
|
5783
|
+
const gone = await pruneEntry(historyRoot, name, { now, dryRun, tempDir, measure });
|
|
5784
|
+
if (gone) pruned.push(gone);
|
|
5785
|
+
} catch (error) {
|
|
5786
|
+
onError(error);
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
return { pruned, finished: true };
|
|
5790
|
+
}
|
|
5791
|
+
async function pruneEntry(historyRoot, name, run) {
|
|
5792
|
+
if (name.startsWith(PRUNED_PREFIX)) {
|
|
5793
|
+
if (!run.dryRun) await rm2(join19(historyRoot, name), { recursive: true, force: true });
|
|
5794
|
+
return null;
|
|
5795
|
+
}
|
|
5796
|
+
return isHistoryId(name) ? pruneOne(historyRoot, name, run) : null;
|
|
5797
|
+
}
|
|
5798
|
+
async function pruneOne(historyRoot, id, run) {
|
|
5799
|
+
const home = join19(historyRoot, id);
|
|
5800
|
+
if (abandonedProject(home, id, run.now, run.tempDir) === null) return null;
|
|
5801
|
+
const release = await takeUnlessBusy(home);
|
|
5802
|
+
if (!release) return null;
|
|
5803
|
+
let taken;
|
|
5804
|
+
try {
|
|
5805
|
+
taken = recheckAndMoveAway(historyRoot, id, run);
|
|
5806
|
+
} catch (error) {
|
|
5807
|
+
release();
|
|
5808
|
+
throw error;
|
|
5809
|
+
}
|
|
5810
|
+
if (!taken?.trash) release();
|
|
5811
|
+
if (!taken) return null;
|
|
5812
|
+
const bytes = await clearAway(taken.trash, home, run.measure);
|
|
5813
|
+
return { id, projectDir: taken.projectDir, bytes };
|
|
5814
|
+
}
|
|
5815
|
+
async function clearAway(trash, home, measure) {
|
|
5816
|
+
const bytes = measure ? await bytesIn(trash ?? home) : 0;
|
|
5817
|
+
if (trash) await rm2(trash, { recursive: true, force: true });
|
|
5818
|
+
return bytes;
|
|
5819
|
+
}
|
|
5820
|
+
async function takeUnlessBusy(home) {
|
|
5821
|
+
try {
|
|
5822
|
+
return await takeHistoryOwnership(home, 0);
|
|
5823
|
+
} catch (error) {
|
|
5824
|
+
if (error instanceof HistoryBusyError) return null;
|
|
5825
|
+
throw error;
|
|
5826
|
+
}
|
|
5827
|
+
}
|
|
5828
|
+
function recheckAndMoveAway(historyRoot, id, { now, dryRun, tempDir }) {
|
|
5829
|
+
const home = join19(historyRoot, id);
|
|
5830
|
+
const projectDir = abandonedProject(home, id, now, tempDir);
|
|
5831
|
+
if (projectDir === null) return null;
|
|
5832
|
+
if (dryRun) return { projectDir, trash: null };
|
|
5833
|
+
const trash = join19(historyRoot, `${PRUNED_PREFIX}${id}-${randomUUID8()}`);
|
|
5834
|
+
renameSync5(home, trash);
|
|
5835
|
+
return { projectDir, trash };
|
|
5836
|
+
}
|
|
5837
|
+
function abandonedProject(home, id, now, tempDir) {
|
|
5838
|
+
const record = readRecord(home);
|
|
5839
|
+
const dir = record?.dir;
|
|
5840
|
+
if (typeof dir !== "string") return null;
|
|
5841
|
+
if (now - lastUsed(home) < KEEP_GONE_PROJECT_HISTORY_MS && !isSafePath(tempDir, dir)) return null;
|
|
5842
|
+
return projectGone(dir, id, record?.dev, tempDir) ? dir : null;
|
|
5843
|
+
}
|
|
5844
|
+
function projectGone(dir, id, dev, tempDir) {
|
|
5845
|
+
if (!statSync9(join19(dir, ID_PATH), { throwIfNoEntry: false }))
|
|
5846
|
+
return diskStillHere(dir, dev, tempDir);
|
|
5847
|
+
const there = readId(dir);
|
|
5848
|
+
return there !== null && there !== id;
|
|
5849
|
+
}
|
|
5850
|
+
function diskStillHere(dir, dev, tempDir) {
|
|
5851
|
+
let at = dir;
|
|
5852
|
+
let stat3 = statSync9(at, { throwIfNoEntry: false });
|
|
5853
|
+
while (!stat3) {
|
|
5854
|
+
if (dirname8(at) === at) return false;
|
|
5855
|
+
at = dirname8(at);
|
|
5856
|
+
stat3 = statSync9(at, { throwIfNoEntry: false });
|
|
5857
|
+
}
|
|
5858
|
+
if (typeof dev === "number") return stat3.dev === dev;
|
|
5859
|
+
return at === dir || isSafePath(tempDir, at);
|
|
5860
|
+
}
|
|
5861
|
+
function lastUsed(home) {
|
|
5862
|
+
const mtime = (file) => statSync9(join19(home, file), { throwIfNoEntry: false })?.mtimeMs ?? 0;
|
|
5863
|
+
return Math.max(mtime("project.json"), mtime("log.jsonl"));
|
|
5864
|
+
}
|
|
5865
|
+
async function bytesIn(dir) {
|
|
5866
|
+
let total = 0;
|
|
5867
|
+
for (const entry of await readdir2(dir, { withFileTypes: true })) {
|
|
5868
|
+
const path = join19(dir, entry.name);
|
|
5869
|
+
total += entry.isDirectory() ? await bytesIn(path) : (await lstat(path)).size;
|
|
5870
|
+
}
|
|
5871
|
+
return total;
|
|
5872
|
+
}
|
|
5873
|
+
function prunedWithinADay(stamp, now) {
|
|
5874
|
+
try {
|
|
5875
|
+
const last = Number(readFileSync17(stamp, "utf-8"));
|
|
5876
|
+
return last <= now && now - last < DAY_MS;
|
|
5877
|
+
} catch {
|
|
5878
|
+
return false;
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
var pruning = /* @__PURE__ */ new Set();
|
|
5882
|
+
function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs } = {}) {
|
|
5883
|
+
const stamp = join19(historyRoot, LAST_PRUNE_FILE);
|
|
5884
|
+
if (pruning.has(historyRoot) || prunedWithinADay(stamp, now)) return;
|
|
5885
|
+
pruning.add(historyRoot);
|
|
5886
|
+
setImmediate(() => {
|
|
5887
|
+
pruneWithin(historyRoot, { now, onError, budgetMs, measure: false }).then(({ finished }) => finished && writeFileSync9(stamp, String(now))).catch((error) => onError?.(error)).finally(() => pruning.delete(historyRoot));
|
|
5888
|
+
});
|
|
5889
|
+
}
|
|
5890
|
+
|
|
5655
5891
|
// src/history/historyLog.ts
|
|
5656
|
-
import { closeSync as closeSync5, constants as constants4, mkdirSync as
|
|
5657
|
-
import { dirname as
|
|
5892
|
+
import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync4, openSync as openSync5, readFileSync as readFileSync18, writeSync as writeSync2 } from "fs";
|
|
5893
|
+
import { dirname as dirname9 } from "path";
|
|
5658
5894
|
var START = "start";
|
|
5659
5895
|
function parseRecord(line) {
|
|
5660
5896
|
try {
|
|
@@ -5666,7 +5902,7 @@ function parseRecord(line) {
|
|
|
5666
5902
|
function readLog(file, onUnreadable) {
|
|
5667
5903
|
let text2;
|
|
5668
5904
|
try {
|
|
5669
|
-
text2 =
|
|
5905
|
+
text2 = readFileSync18(file, "utf-8");
|
|
5670
5906
|
} catch {
|
|
5671
5907
|
return null;
|
|
5672
5908
|
}
|
|
@@ -5706,7 +5942,7 @@ function writeLog(file, log) {
|
|
|
5706
5942
|
...log.entries.map((entry) => ({ type: "entry", entry })),
|
|
5707
5943
|
...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
|
|
5708
5944
|
];
|
|
5709
|
-
|
|
5945
|
+
mkdirSync4(dirname9(file), { recursive: true });
|
|
5710
5946
|
replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
|
|
5711
5947
|
`).join(""), 420);
|
|
5712
5948
|
}
|
|
@@ -5779,7 +6015,7 @@ function referencedHashes(log, current) {
|
|
|
5779
6015
|
}
|
|
5780
6016
|
|
|
5781
6017
|
// src/history/projectHistory.ts
|
|
5782
|
-
var DEFAULT_HISTORY_ROOT =
|
|
6018
|
+
var DEFAULT_HISTORY_ROOT = join20(homedir2(), ".cache", "hyperframes", "history");
|
|
5783
6019
|
var MAX_WINDOW_IDLE_MS = 10 * 6e4;
|
|
5784
6020
|
var UNDO_MODES = ["just-this", "back-to-before", "keep-later-edits"];
|
|
5785
6021
|
var OUTSIDE = { kind: "outside", name: "Outside" };
|
|
@@ -5799,26 +6035,26 @@ var isMissingFolder = (error) => ["ENOENT", "ENOTDIR"].includes(error.code ?? ""
|
|
|
5799
6035
|
async function removeEmptyFolders(dir) {
|
|
5800
6036
|
let entries;
|
|
5801
6037
|
try {
|
|
5802
|
-
entries = await
|
|
6038
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
5803
6039
|
} catch (error) {
|
|
5804
6040
|
if (isMissingFolder(error)) return;
|
|
5805
6041
|
throw error;
|
|
5806
6042
|
}
|
|
5807
6043
|
for (const entry of entries)
|
|
5808
|
-
if (entry.isDirectory()) await removeEmptyFolders(
|
|
6044
|
+
if (entry.isDirectory()) await removeEmptyFolders(join20(dir, entry.name));
|
|
5809
6045
|
await rmdir(dir);
|
|
5810
6046
|
}
|
|
5811
6047
|
function fileLeftIn(dir, at, deleted) {
|
|
5812
|
-
for (const entry of
|
|
6048
|
+
for (const entry of readdirSync7(dir, { withFileTypes: true })) {
|
|
5813
6049
|
const path = `${at}/${entry.name}`;
|
|
5814
|
-
const left = entry.isDirectory() ? fileLeftIn(
|
|
6050
|
+
const left = entry.isDirectory() ? fileLeftIn(join20(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
|
|
5815
6051
|
if (left) return left;
|
|
5816
6052
|
}
|
|
5817
6053
|
return void 0;
|
|
5818
6054
|
}
|
|
5819
6055
|
function ignoresCase(dir) {
|
|
5820
|
-
const self =
|
|
5821
|
-
const other =
|
|
6056
|
+
const self = statSync10(join20(dir, ID_PATH), { throwIfNoEntry: false });
|
|
6057
|
+
const other = statSync10(join20(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
|
|
5822
6058
|
return self !== void 0 && other?.ino === self.ino && other.dev === self.dev;
|
|
5823
6059
|
}
|
|
5824
6060
|
var blocks = (removed, added) => added.startsWith(`${removed}/`) || removed.startsWith(`${added}/`);
|
|
@@ -5835,9 +6071,9 @@ var Engine = class {
|
|
|
5835
6071
|
this.projectId = projectId;
|
|
5836
6072
|
this.blobs = blobs;
|
|
5837
6073
|
this.dir = resolve8(options.projectDir);
|
|
5838
|
-
this.home =
|
|
6074
|
+
this.home = join20(options.historyRoot, projectId);
|
|
5839
6075
|
this.foldsCase = ignoresCase(this.dir);
|
|
5840
|
-
const folder =
|
|
6076
|
+
const folder = statSync10(this.dir, { throwIfNoEntry: false });
|
|
5841
6077
|
if (!folder || readId(this.dir) !== projectId || !isRecordedFolder(this.home, folder))
|
|
5842
6078
|
throw this.replaced();
|
|
5843
6079
|
this.folder = { dev: folder.dev, ino: folder.ino, birthtimeMs: folder.birthtimeMs };
|
|
@@ -5868,7 +6104,7 @@ var Engine = class {
|
|
|
5868
6104
|
return this.foldsCase ? path.toLowerCase() : path;
|
|
5869
6105
|
}
|
|
5870
6106
|
get logFile() {
|
|
5871
|
-
return
|
|
6107
|
+
return join20(this.home, "log.jsonl");
|
|
5872
6108
|
}
|
|
5873
6109
|
now() {
|
|
5874
6110
|
return (this.options.now ?? Date.now)();
|
|
@@ -5883,7 +6119,7 @@ var Engine = class {
|
|
|
5883
6119
|
}
|
|
5884
6120
|
/** Another project if the folder or its id changed, or the id is gone: a recreated folder may reuse the inode. */
|
|
5885
6121
|
whereFolder() {
|
|
5886
|
-
const now =
|
|
6122
|
+
const now = statSync10(this.dir, { throwIfNoEntry: false });
|
|
5887
6123
|
if (!now) return "gone";
|
|
5888
6124
|
const same = now.dev === this.folder.dev && sameFolder(now, this.folder);
|
|
5889
6125
|
return same && readId(this.dir) === this.projectId ? "here" : "replaced";
|
|
@@ -5896,7 +6132,7 @@ var Engine = class {
|
|
|
5896
6132
|
persistLog(record) {
|
|
5897
6133
|
if (record) saveRecord(this.logFile, this.log, record);
|
|
5898
6134
|
else writeLog(this.logFile, this.log);
|
|
5899
|
-
if (!existsSync11(
|
|
6135
|
+
if (!existsSync11(join20(this.home, "project.json")))
|
|
5900
6136
|
recordProject(this.home, this.dir, this.folder);
|
|
5901
6137
|
}
|
|
5902
6138
|
replaced() {
|
|
@@ -5946,7 +6182,7 @@ var Engine = class {
|
|
|
5946
6182
|
}
|
|
5947
6183
|
readStatCache() {
|
|
5948
6184
|
try {
|
|
5949
|
-
const files = JSON.parse(
|
|
6185
|
+
const files = JSON.parse(readFileSync19(join20(this.home, "stat.json"), "utf-8"));
|
|
5950
6186
|
return new Map(Object.entries(files));
|
|
5951
6187
|
} catch {
|
|
5952
6188
|
return /* @__PURE__ */ new Map();
|
|
@@ -5954,8 +6190,8 @@ var Engine = class {
|
|
|
5954
6190
|
}
|
|
5955
6191
|
saveStatCache() {
|
|
5956
6192
|
const files = JSON.stringify(Object.fromEntries(this.tracked));
|
|
5957
|
-
|
|
5958
|
-
replaceFileAtomically(
|
|
6193
|
+
mkdirSync5(this.home, { recursive: true });
|
|
6194
|
+
replaceFileAtomically(join20(this.home, "stat.json"), files, 420);
|
|
5959
6195
|
}
|
|
5960
6196
|
/**
|
|
5961
6197
|
* Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
|
|
@@ -5995,7 +6231,7 @@ var Engine = class {
|
|
|
5995
6231
|
/** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
|
|
5996
6232
|
async storeIfPresent(path) {
|
|
5997
6233
|
try {
|
|
5998
|
-
return await this.blobs.put(
|
|
6234
|
+
return await this.blobs.put(join20(this.dir, path));
|
|
5999
6235
|
} catch (error) {
|
|
6000
6236
|
if (error.code === "ENOENT") return null;
|
|
6001
6237
|
throw error;
|
|
@@ -6082,7 +6318,7 @@ var Engine = class {
|
|
|
6082
6318
|
}
|
|
6083
6319
|
hits.forEach(
|
|
6084
6320
|
({ change }, i) => forgetOverwrittenBytes(
|
|
6085
|
-
|
|
6321
|
+
join20(this.dir, change.path),
|
|
6086
6322
|
new Set([...walked[i]].map(hashVersion))
|
|
6087
6323
|
)
|
|
6088
6324
|
);
|
|
@@ -6090,7 +6326,7 @@ var Engine = class {
|
|
|
6090
6326
|
}
|
|
6091
6327
|
/** Walks the server's writes back from `change.after` to the bytes they replaced, stored so an undo restores them. */
|
|
6092
6328
|
async overwrittenBy(change, told, seen) {
|
|
6093
|
-
const absPath =
|
|
6329
|
+
const absPath = join20(this.dir, change.path);
|
|
6094
6330
|
let bytes;
|
|
6095
6331
|
for (let hash = change.after; hash; ) {
|
|
6096
6332
|
if (seen.has(hash)) return void 0;
|
|
@@ -6102,12 +6338,12 @@ var Engine = class {
|
|
|
6102
6338
|
if (hash === change.before || hash === told) return hash;
|
|
6103
6339
|
}
|
|
6104
6340
|
if (bytes === void 0) return void 0;
|
|
6105
|
-
const staged =
|
|
6341
|
+
const staged = join20(this.home, `overwrote-${randomUUID9()}`);
|
|
6106
6342
|
await writeFile(staged, bytes);
|
|
6107
6343
|
try {
|
|
6108
6344
|
return await this.blobs.put(staged);
|
|
6109
6345
|
} finally {
|
|
6110
|
-
await
|
|
6346
|
+
await rm3(staged, { force: true });
|
|
6111
6347
|
}
|
|
6112
6348
|
}
|
|
6113
6349
|
cutOut(group, change, cut) {
|
|
@@ -6157,7 +6393,7 @@ var Engine = class {
|
|
|
6157
6393
|
return this.outside;
|
|
6158
6394
|
}
|
|
6159
6395
|
newGroup(who, label) {
|
|
6160
|
-
return { id:
|
|
6396
|
+
return { id: randomUUID9(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
|
|
6161
6397
|
}
|
|
6162
6398
|
async commitOutside() {
|
|
6163
6399
|
if (this.outside?.changes.size) await this.commitClaim();
|
|
@@ -6274,7 +6510,7 @@ var Engine = class {
|
|
|
6274
6510
|
const parts = path.split("/");
|
|
6275
6511
|
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
6276
6512
|
const at = parts.slice(0, depth).join("/");
|
|
6277
|
-
const stat3 = lstatSync4(
|
|
6513
|
+
const stat3 = lstatSync4(join20(this.dir, at), { throwIfNoEntry: false });
|
|
6278
6514
|
if (!stat3) return void 0;
|
|
6279
6515
|
if (at === path || !stat3.isDirectory()) return { at, stat: stat3 };
|
|
6280
6516
|
}
|
|
@@ -6286,7 +6522,7 @@ var Engine = class {
|
|
|
6286
6522
|
if (!standing) return void 0;
|
|
6287
6523
|
if (standing.stat.isSymbolicLink()) return standing.at;
|
|
6288
6524
|
if (standing.at === path && !standing.stat.isDirectory()) return void 0;
|
|
6289
|
-
const real = realpathSync.native(
|
|
6525
|
+
const real = realpathSync.native(join20(this.dir, standing.at));
|
|
6290
6526
|
const at = relative5(realpathSync.native(this.dir), real).split(sep3).join("/");
|
|
6291
6527
|
if (standing.at !== path) return deleted.has(at) ? void 0 : at;
|
|
6292
6528
|
return fileLeftIn(real, at, deleted);
|
|
@@ -6311,7 +6547,7 @@ var Engine = class {
|
|
|
6311
6547
|
/** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
|
|
6312
6548
|
async writeProjectFile(path, hash) {
|
|
6313
6549
|
this.assertWritable();
|
|
6314
|
-
const absPath =
|
|
6550
|
+
const absPath = join20(this.dir, path);
|
|
6315
6551
|
const { writeToken } = this;
|
|
6316
6552
|
if (writeToken) {
|
|
6317
6553
|
const version = hash === null ? DELETED_VERSION : hashVersion(hash);
|
|
@@ -6445,14 +6681,14 @@ var Engine = class {
|
|
|
6445
6681
|
const dest = resolve8(emptyDir);
|
|
6446
6682
|
if (isWithin(this.dir, dest))
|
|
6447
6683
|
throw new Error(`Checkout writes outside the project only: ${dest}`);
|
|
6448
|
-
if ((await
|
|
6684
|
+
if ((await readdir3(dest).catch(missingIsEmpty)).length > 0)
|
|
6449
6685
|
throw new Error(`Checkout writes into an empty folder only: ${dest}`);
|
|
6450
6686
|
const files = manifestAround(this.log, entryId, side);
|
|
6451
6687
|
try {
|
|
6452
|
-
for (const [path, hash] of files) await this.blobs.writeTo(hash,
|
|
6688
|
+
for (const [path, hash] of files) await this.blobs.writeTo(hash, join20(dest, path));
|
|
6453
6689
|
} catch (error) {
|
|
6454
|
-
for (const name of await
|
|
6455
|
-
await
|
|
6690
|
+
for (const name of await readdir3(dest).catch(() => []))
|
|
6691
|
+
await rm3(join20(dest, name), { recursive: true, force: true });
|
|
6456
6692
|
throw error;
|
|
6457
6693
|
}
|
|
6458
6694
|
}),
|
|
@@ -6488,7 +6724,7 @@ var Engine = class {
|
|
|
6488
6724
|
};
|
|
6489
6725
|
function isWithin(dir, path) {
|
|
6490
6726
|
let probe = resolve8(path);
|
|
6491
|
-
while (!existsSync11(probe) &&
|
|
6727
|
+
while (!existsSync11(probe) && dirname10(probe) !== probe) probe = dirname10(probe);
|
|
6492
6728
|
const rel = relative5(realpathSync.native(dir), realpathSync.native(probe));
|
|
6493
6729
|
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute2(rel);
|
|
6494
6730
|
}
|
|
@@ -6507,13 +6743,17 @@ async function openProjectHistory(options) {
|
|
|
6507
6743
|
throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
|
|
6508
6744
|
const projectId = projectHistoryId(options.projectDir, options.historyRoot);
|
|
6509
6745
|
const release = await takeHistoryOwnership(
|
|
6510
|
-
|
|
6746
|
+
join20(options.historyRoot, projectId),
|
|
6511
6747
|
options.ownerWaitMs ?? 5e3
|
|
6512
6748
|
);
|
|
6513
6749
|
try {
|
|
6514
|
-
const blobs = await openBlobStore(
|
|
6750
|
+
const blobs = await openBlobStore(join20(options.historyRoot, projectId, "blobs"));
|
|
6515
6751
|
const engine = new Engine(options, projectId, blobs);
|
|
6516
6752
|
await engine.queue(() => engine.start());
|
|
6753
|
+
pruneGoneProjectHistoriesDaily(options.historyRoot, engine.now(), {
|
|
6754
|
+
onError: options.onError,
|
|
6755
|
+
budgetMs: options.pruneGoneProjectsBudgetMs
|
|
6756
|
+
});
|
|
6517
6757
|
const api = engine.api();
|
|
6518
6758
|
return {
|
|
6519
6759
|
...api,
|
|
@@ -6664,6 +6904,21 @@ function registerHistoryRoutes(api, adapter) {
|
|
|
6664
6904
|
// src/createStudioApi.ts
|
|
6665
6905
|
function createStudioApi(adapter) {
|
|
6666
6906
|
const api = new Hono();
|
|
6907
|
+
api.use(async function answerProjectDirMissingAfterErrorHandlers(c, next) {
|
|
6908
|
+
const hostHeaders = new Headers(c.res.headers);
|
|
6909
|
+
await next();
|
|
6910
|
+
if (isProjectRootMissing(c.error)) replaceWithProjectDirMissing(c, hostHeaders);
|
|
6911
|
+
});
|
|
6912
|
+
api.use("/projects/:id/*", async function answerProjectDirMissingForVanishedFolder(c, next) {
|
|
6913
|
+
const hostHeaders = new Headers(c.res.headers);
|
|
6914
|
+
const dirBeforeRoute = await Promise.resolve().then(() => adapter.resolveProject(c.req.param("id"))).then(
|
|
6915
|
+
(project) => project?.dir,
|
|
6916
|
+
() => void 0
|
|
6917
|
+
);
|
|
6918
|
+
await next();
|
|
6919
|
+
if (c.res.status >= 403 && dirBeforeRoute && folderGone(dirBeforeRoute))
|
|
6920
|
+
replaceWithProjectDirMissing(c, hostHeaders);
|
|
6921
|
+
});
|
|
6667
6922
|
registerProjectRoutes(api, adapter);
|
|
6668
6923
|
registerFileRoutes(api, adapter);
|
|
6669
6924
|
registerPreviewRoutes(api, adapter);
|
|
@@ -6760,26 +7015,27 @@ function numericAttribute(element, name) {
|
|
|
6760
7015
|
|
|
6761
7016
|
// src/helpers/previewDocumentStore.ts
|
|
6762
7017
|
import { createHash as createHash8 } from "crypto";
|
|
6763
|
-
import {
|
|
6764
|
-
import { join as
|
|
6765
|
-
function createPreviewDocumentStore(
|
|
6766
|
-
const
|
|
7018
|
+
import { readFileSync as readFileSync20, readdirSync as readdirSync8, rmSync as rmSync7 } from "fs";
|
|
7019
|
+
import { join as join21 } from "path";
|
|
7020
|
+
function createPreviewDocumentStore(projectDir, salt) {
|
|
7021
|
+
const dir = join21(projectDir, ".hyperframes", "preview");
|
|
7022
|
+
const fileFor = (key) => join21(dir, `${createHash8("sha256").update(`${salt}
|
|
6767
7023
|
${key}`).digest("hex").slice(0, 32)}.html`);
|
|
6768
7024
|
return {
|
|
6769
7025
|
read(key) {
|
|
6770
7026
|
try {
|
|
6771
|
-
return
|
|
7027
|
+
return readFileSync20(fileFor(key), "utf-8");
|
|
6772
7028
|
} catch {
|
|
6773
7029
|
return null;
|
|
6774
7030
|
}
|
|
6775
7031
|
},
|
|
6776
7032
|
write(key, html) {
|
|
6777
7033
|
try {
|
|
6778
|
-
|
|
7034
|
+
mkdirWithinProject(projectDir, dir);
|
|
6779
7035
|
const file = fileFor(key);
|
|
6780
7036
|
replaceFileAtomically(file, html, 420);
|
|
6781
|
-
for (const name of
|
|
6782
|
-
if (
|
|
7037
|
+
for (const name of readdirSync8(dir)) {
|
|
7038
|
+
if (join21(dir, name) !== file) rmSync7(join21(dir, name), { force: true });
|
|
6783
7039
|
}
|
|
6784
7040
|
} catch {
|
|
6785
7041
|
}
|
|
@@ -6889,9 +7145,11 @@ export {
|
|
|
6889
7145
|
historyCache,
|
|
6890
7146
|
identifyFileWrite,
|
|
6891
7147
|
isSafePath,
|
|
7148
|
+
listProjectHistories,
|
|
6892
7149
|
openProjectHistory,
|
|
6893
7150
|
parseSourceDocument,
|
|
6894
7151
|
patchElementInHtml,
|
|
7152
|
+
pruneGoneProjectHistories,
|
|
6895
7153
|
removeElementFromHtml,
|
|
6896
7154
|
requestSubPath,
|
|
6897
7155
|
settledFileTag,
|