@hyperframes/studio-server 0.8.79 → 0.8.80
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-45POWYFS.js → chunk-56JREE33.js} +2 -2
- package/dist/{chunk-4MNTZVVZ.js → chunk-KOB22VPG.js} +14 -10
- package/dist/chunk-KOB22VPG.js.map +1 -0
- package/dist/{chunk-QYDRVMJ6.js → chunk-P5EJPIH3.js} +5 -3
- package/dist/chunk-P5EJPIH3.js.map +1 -0
- package/dist/{chunk-X3T65KC2.js → chunk-YGANSFUU.js} +3 -3
- package/dist/helpers/mediaCodecMap.d.ts +7 -7
- 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.js +2 -2
- package/dist/helpers/sourceMutation.js +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +454 -262
- package/dist/index.js.map +1 -1
- package/dist/{mediaProxyPreview-rAYw-Jeh.d.ts → mediaProxyPreview-B7ckRI-l.d.ts} +1 -1
- package/package.json +4 -3
- package/dist/chunk-4MNTZVVZ.js.map +0 -1
- package/dist/chunk-QYDRVMJ6.js.map +0 -1
- /package/dist/{chunk-45POWYFS.js.map → chunk-56JREE33.js.map} +0 -0
- /package/dist/{chunk-X3T65KC2.js.map → chunk-YGANSFUU.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-P5EJPIH3.js";
|
|
17
17
|
import {
|
|
18
18
|
getElementScreenshotClip
|
|
19
19
|
} from "./chunk-W2SBTCO2.js";
|
|
@@ -22,12 +22,12 @@ import {
|
|
|
22
22
|
isAutoProxyEnabled,
|
|
23
23
|
proxyEtagSalt,
|
|
24
24
|
resolvePreviewMediaCodecProbeCache
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-YGANSFUU.js";
|
|
26
26
|
import {
|
|
27
27
|
ProxyCapacityError,
|
|
28
28
|
ProxyTranscodeError,
|
|
29
29
|
resolveProxy
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-56JREE33.js";
|
|
31
31
|
import {
|
|
32
32
|
PROXY_VARIANT_CONFIG,
|
|
33
33
|
decideMediaProxyEligibility,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
probeMediaMetadata,
|
|
37
37
|
recordProxyRequest,
|
|
38
38
|
resolveProxyVariantRequest
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-KOB22VPG.js";
|
|
40
40
|
import {
|
|
41
41
|
STUDIO_MANUAL_EDITS_PATH,
|
|
42
42
|
createStudioManualEditsRenderBodyScript,
|
|
@@ -289,22 +289,22 @@ function collectProjectSignatureFiles(projectDir, dir, files) {
|
|
|
289
289
|
if (SIGNATURE_EXCLUDED_DIRS.has(entry)) continue;
|
|
290
290
|
const file = resolve(dir, entry);
|
|
291
291
|
if (!isPathWithin(projectDir, file)) continue;
|
|
292
|
-
let
|
|
292
|
+
let stat3;
|
|
293
293
|
try {
|
|
294
|
-
|
|
294
|
+
stat3 = lstatSync(file);
|
|
295
295
|
} catch {
|
|
296
296
|
continue;
|
|
297
297
|
}
|
|
298
|
-
if (
|
|
299
|
-
if (
|
|
298
|
+
if (stat3.isSymbolicLink()) continue;
|
|
299
|
+
if (stat3.isDirectory()) {
|
|
300
300
|
collectProjectSignatureFiles(projectDir, file, files);
|
|
301
|
-
} else if (
|
|
301
|
+
} else if (stat3.isFile()) {
|
|
302
302
|
files.push({
|
|
303
303
|
file,
|
|
304
|
-
mtimeMs:
|
|
305
|
-
ctimeMs:
|
|
306
|
-
size:
|
|
307
|
-
textContentEligible: isTextContentEligible(file,
|
|
304
|
+
mtimeMs: stat3.mtimeMs,
|
|
305
|
+
ctimeMs: stat3.ctimeMs,
|
|
306
|
+
size: stat3.size,
|
|
307
|
+
textContentEligible: isTextContentEligible(file, stat3.size)
|
|
308
308
|
});
|
|
309
309
|
}
|
|
310
310
|
}
|
|
@@ -314,19 +314,19 @@ function collectProjectSignatureManifestFiles(projectDir, files) {
|
|
|
314
314
|
for (const manifestPath of STUDIO_SIGNATURE_MANIFEST_PATHS) {
|
|
315
315
|
const file = resolve(projectDir, manifestPath);
|
|
316
316
|
if (seen.has(file) || !isPathWithin(projectDir, file)) continue;
|
|
317
|
-
let
|
|
317
|
+
let stat3;
|
|
318
318
|
try {
|
|
319
|
-
|
|
319
|
+
stat3 = lstatSync(file);
|
|
320
320
|
} catch {
|
|
321
321
|
continue;
|
|
322
322
|
}
|
|
323
|
-
if (
|
|
323
|
+
if (stat3.isSymbolicLink() || !stat3.isFile()) continue;
|
|
324
324
|
files.push({
|
|
325
325
|
file,
|
|
326
|
-
mtimeMs:
|
|
327
|
-
ctimeMs:
|
|
328
|
-
size:
|
|
329
|
-
textContentEligible: isTextContentEligible(file,
|
|
326
|
+
mtimeMs: stat3.mtimeMs,
|
|
327
|
+
ctimeMs: stat3.ctimeMs,
|
|
328
|
+
size: stat3.size,
|
|
329
|
+
textContentEligible: isTextContentEligible(file, stat3.size)
|
|
330
330
|
});
|
|
331
331
|
seen.add(file);
|
|
332
332
|
}
|
|
@@ -761,9 +761,9 @@ function hashVersion(hex) {
|
|
|
761
761
|
function hashOfVersion(version) {
|
|
762
762
|
return /^"sha256:([0-9a-f]{64})"$/.exec(version)?.[1];
|
|
763
763
|
}
|
|
764
|
-
function settledFileTag(
|
|
765
|
-
if (now -
|
|
766
|
-
return [
|
|
764
|
+
function settledFileTag(stat3, now = Date.now()) {
|
|
765
|
+
if (now - stat3.ctimeMs < 3e3) return null;
|
|
766
|
+
return [stat3.ino, stat3.ctimeMs, stat3.size].map((n) => n.toString(36)).join("-");
|
|
767
767
|
}
|
|
768
768
|
function createWriteToken(requestToken) {
|
|
769
769
|
const token = requestToken?.trim();
|
|
@@ -2776,10 +2776,10 @@ function registerFileRoutes(api, adapter) {
|
|
|
2776
2776
|
api.delete("/projects/:id/files/*", async (c) => {
|
|
2777
2777
|
const res = await resolveProjectFile(c, adapter, { mustExist: true });
|
|
2778
2778
|
if ("error" in res) return res.error;
|
|
2779
|
-
const
|
|
2779
|
+
const stat3 = statSync3(res.absPath);
|
|
2780
2780
|
const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
2781
2781
|
if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
|
|
2782
|
-
if (
|
|
2782
|
+
if (stat3.isDirectory()) {
|
|
2783
2783
|
rmSync3(res.absPath, { recursive: true });
|
|
2784
2784
|
} else {
|
|
2785
2785
|
unlinkSync2(res.absPath);
|
|
@@ -3429,7 +3429,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3429
3429
|
// src/routes/preview.ts
|
|
3430
3430
|
import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
|
|
3431
3431
|
import { Readable } from "stream";
|
|
3432
|
-
import { join as
|
|
3432
|
+
import { join as join10, resolve as resolve5 } from "path";
|
|
3433
3433
|
import { createHash as createHash4 } from "crypto";
|
|
3434
3434
|
import {
|
|
3435
3435
|
addScenePartsManifest,
|
|
@@ -3622,6 +3622,83 @@ function hasBaseElement(html) {
|
|
|
3622
3622
|
// src/routes/preview.ts
|
|
3623
3623
|
import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
|
|
3624
3624
|
|
|
3625
|
+
// src/helpers/previewReads.ts
|
|
3626
|
+
import { readlinkSync as readlinkSync2 } from "fs";
|
|
3627
|
+
import { dirname as dirname6, join as join9, parse, relative as relative5, resolve as resolve4 } from "path";
|
|
3628
|
+
var ALWAYS_AFFECTS = ["hyperframes.json", ...STUDIO_SIGNATURE_MANIFEST_PATHS];
|
|
3629
|
+
var REFERENCE = /\b(?:src|href|poster|data-composition-src)\s*=\s*(?:"([^"\n]*)"|'([^'\n]*)')|url\(\s*(?:"([^"\n]*)"|'([^'\n]*)'|([^"'\s)]+))/gi;
|
|
3630
|
+
var pathKey = process.platform === "darwin" || process.platform === "win32" ? (path) => resolve4(path).toLowerCase() : (path) => resolve4(path);
|
|
3631
|
+
var readsByProject = /* @__PURE__ */ new Map();
|
|
3632
|
+
var builtProjects = /* @__PURE__ */ new Set();
|
|
3633
|
+
var projectKey = (projectDir) => pathKey(realFilePath(resolve4(projectDir)));
|
|
3634
|
+
var inRealProject = (projectDir, path) => join9(realFilePath(resolve4(projectDir)), relative5(resolve4(projectDir), resolve4(projectDir, path)));
|
|
3635
|
+
var readLink = (path) => {
|
|
3636
|
+
try {
|
|
3637
|
+
return readlinkSync2(path);
|
|
3638
|
+
} catch {
|
|
3639
|
+
return null;
|
|
3640
|
+
}
|
|
3641
|
+
};
|
|
3642
|
+
function linksOnTheWay(path) {
|
|
3643
|
+
const passed = [];
|
|
3644
|
+
let at = parse(path).root;
|
|
3645
|
+
let rest = path.slice(at.length).split(/[\\/]+/);
|
|
3646
|
+
for (let hops = 0; rest.length > 0; ) {
|
|
3647
|
+
const part = rest.shift();
|
|
3648
|
+
if (!part || part === ".") continue;
|
|
3649
|
+
if (part === "..") {
|
|
3650
|
+
at = dirname6(at);
|
|
3651
|
+
continue;
|
|
3652
|
+
}
|
|
3653
|
+
const next = join9(at, part);
|
|
3654
|
+
const target = hops < 40 ? readLink(next) : null;
|
|
3655
|
+
if (target === null) {
|
|
3656
|
+
at = next;
|
|
3657
|
+
continue;
|
|
3658
|
+
}
|
|
3659
|
+
hops++;
|
|
3660
|
+
passed.push(next);
|
|
3661
|
+
const root = parse(target).root;
|
|
3662
|
+
if (root) at = root;
|
|
3663
|
+
rest = [...target.slice(root.length).split(/[\\/]+/), ...rest];
|
|
3664
|
+
}
|
|
3665
|
+
passed.push(at);
|
|
3666
|
+
return passed;
|
|
3667
|
+
}
|
|
3668
|
+
function recordPreviewRead(projectDir, filePath) {
|
|
3669
|
+
const key = projectKey(projectDir);
|
|
3670
|
+
let reads = readsByProject.get(key);
|
|
3671
|
+
if (!reads) readsByProject.set(key, reads = /* @__PURE__ */ new Set());
|
|
3672
|
+
const read = inRealProject(projectDir, filePath);
|
|
3673
|
+
for (const found of [read, ...linksOnTheWay(read)]) {
|
|
3674
|
+
for (let path = pathKey(found); !reads.has(path); path = dirname6(path)) reads.add(path);
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
function recordPreviewReferences(projectDir, html) {
|
|
3678
|
+
const named = /* @__PURE__ */ new Set();
|
|
3679
|
+
for (const match of html.matchAll(REFERENCE)) {
|
|
3680
|
+
const url = (match[1] ?? match[2] ?? match[3] ?? match[4] ?? match[5] ?? "").trim();
|
|
3681
|
+
if (!url || /^(?:[a-z][a-z0-9+.-]*:|[/#])/i.test(url)) continue;
|
|
3682
|
+
const path = url.split(/[?#]/)[0] ?? "";
|
|
3683
|
+
let decoded = path;
|
|
3684
|
+
try {
|
|
3685
|
+
decoded = decodeURIComponent(path);
|
|
3686
|
+
} catch {
|
|
3687
|
+
}
|
|
3688
|
+
if (decoded) named.add(decoded);
|
|
3689
|
+
}
|
|
3690
|
+
for (const path of named) recordPreviewRead(projectDir, path);
|
|
3691
|
+
}
|
|
3692
|
+
function recordPreviewBuilt(projectDir) {
|
|
3693
|
+
for (const path of ALWAYS_AFFECTS) recordPreviewRead(projectDir, path);
|
|
3694
|
+
builtProjects.add(projectKey(projectDir));
|
|
3695
|
+
}
|
|
3696
|
+
function affectsPreview(projectDir, changedPath) {
|
|
3697
|
+
const key = projectKey(projectDir);
|
|
3698
|
+
if (!builtProjects.has(key)) return true;
|
|
3699
|
+
return readsByProject.get(key)?.has(pathKey(inRealProject(projectDir, changedPath))) ?? false;
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3625
3702
|
// src/helpers/variablesPayload.ts
|
|
3626
3703
|
var VARIABLES_PAYLOAD_ERROR = "variables must be a JSON object of {variableId: value}";
|
|
3627
3704
|
function isVariablesPayload(value) {
|
|
@@ -3663,7 +3740,7 @@ function injectProjectSignature(html, signature) {
|
|
|
3663
3740
|
${html}`;
|
|
3664
3741
|
}
|
|
3665
3742
|
function readStudioMotionManifestContent(projectDir) {
|
|
3666
|
-
const manifestPath =
|
|
3743
|
+
const manifestPath = join10(projectDir, STUDIO_MOTION_PATH);
|
|
3667
3744
|
if (!existsSync5(manifestPath)) return "";
|
|
3668
3745
|
try {
|
|
3669
3746
|
return readFileSync8(manifestPath, "utf-8");
|
|
@@ -3779,9 +3856,9 @@ function variablesEtagSalt(raw) {
|
|
|
3779
3856
|
return `:vars:${createHash4("sha1").update(raw).digest("hex").slice(0, 12)}`;
|
|
3780
3857
|
}
|
|
3781
3858
|
function previewVariablesFromRequest(rawVariables) {
|
|
3782
|
-
const
|
|
3783
|
-
if (!
|
|
3784
|
-
return { raw: rawVariables, values:
|
|
3859
|
+
const parse2 = parsePreviewVariablesParam(rawVariables);
|
|
3860
|
+
if (!parse2.ok) return { error: parse2.error };
|
|
3861
|
+
return { raw: rawVariables, values: parse2.values };
|
|
3785
3862
|
}
|
|
3786
3863
|
function injectStudioPreviewAugmentations(html, adapter, projectDir, activeCompositionPath) {
|
|
3787
3864
|
return injectStudioMotionScript(
|
|
@@ -3808,14 +3885,16 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
|
|
|
3808
3885
|
}
|
|
3809
3886
|
}
|
|
3810
3887
|
function resolveProjectMainHtml(projectDir, projectId) {
|
|
3811
|
-
const indexPath =
|
|
3888
|
+
const indexPath = join10(projectDir, "index.html");
|
|
3889
|
+
const blockHtmlPath = join10(projectDir, `${projectId}.html`);
|
|
3890
|
+
recordPreviewRead(projectDir, indexPath);
|
|
3891
|
+
recordPreviewRead(projectDir, blockHtmlPath);
|
|
3812
3892
|
if (existsSync5(indexPath)) {
|
|
3813
3893
|
return {
|
|
3814
3894
|
html: readFileSync8(indexPath, "utf-8"),
|
|
3815
3895
|
compositionPath: "index.html"
|
|
3816
3896
|
};
|
|
3817
3897
|
}
|
|
3818
|
-
const blockHtmlPath = join9(projectDir, `${projectId}.html`);
|
|
3819
3898
|
if (existsSync5(blockHtmlPath)) {
|
|
3820
3899
|
return {
|
|
3821
3900
|
html: readFileSync8(blockHtmlPath, "utf-8"),
|
|
@@ -3847,13 +3926,18 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3847
3926
|
const diskMain = resolveProjectMainHtml(project.dir, project.id);
|
|
3848
3927
|
const normalizedDisk = diskMain ? ensureHfIds3(diskMain.html) : null;
|
|
3849
3928
|
try {
|
|
3850
|
-
let bundled = await adapter.bundle(project.dir, {
|
|
3929
|
+
let bundled = await adapter.bundle(project.dir, {
|
|
3930
|
+
stampHfIds: true,
|
|
3931
|
+
onRead: (filePath) => recordPreviewRead(project.dir, filePath)
|
|
3932
|
+
});
|
|
3851
3933
|
let mainCompositionPath = "index.html";
|
|
3852
3934
|
if (!bundled) {
|
|
3853
3935
|
if (!diskMain) return null;
|
|
3854
3936
|
bundled = stripEmbeddedRuntimeScripts2(normalizedDisk ?? diskMain.html);
|
|
3855
3937
|
mainCompositionPath = diskMain.compositionPath;
|
|
3856
3938
|
}
|
|
3939
|
+
recordPreviewReferences(project.dir, bundled);
|
|
3940
|
+
recordPreviewBuilt(project.dir);
|
|
3857
3941
|
if (!bundled.includes("hyperframe.runtime") && !bundled.includes("hyperframes-preview-runtime")) {
|
|
3858
3942
|
const runtimeTag = `<script src="${adapter.runtimeUrl}"></script>`;
|
|
3859
3943
|
bundled = insertBeforeCloseTag2(bundled, "body", `${runtimeTag}
|
|
@@ -3957,6 +4041,8 @@ ${etag}`;
|
|
|
3957
4041
|
const previewVariables = vars.values;
|
|
3958
4042
|
const compPath = requestSubPath(c.req.url, "projects/:id/preview/comp");
|
|
3959
4043
|
const compFile = resolveWithinProject2(project.dir, compPath);
|
|
4044
|
+
if (compFile) recordPreviewRead(project.dir, compFile);
|
|
4045
|
+
recordPreviewRead(project.dir, "index.html");
|
|
3960
4046
|
if (!compFile || !existsSync5(compFile) || !statSync4(compFile).isFile()) {
|
|
3961
4047
|
return c.text("not found", 404);
|
|
3962
4048
|
}
|
|
@@ -3980,6 +4066,7 @@ ${etag}`;
|
|
|
3980
4066
|
stamped
|
|
3981
4067
|
);
|
|
3982
4068
|
if (!html) return c.text("not found", 404);
|
|
4069
|
+
recordPreviewReferences(project.dir, html);
|
|
3983
4070
|
html = ensureHfIds3(await transformPreviewHtml(html, adapter, project, compPath));
|
|
3984
4071
|
html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath);
|
|
3985
4072
|
if (previewVariables) html = injectPreviewVariables(html, previewVariables);
|
|
@@ -3990,13 +4077,14 @@ ${etag}`;
|
|
|
3990
4077
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
3991
4078
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
3992
4079
|
const subPath = requestSubPath(c.req.url, "projects/:id/preview");
|
|
3993
|
-
const candidate =
|
|
4080
|
+
const candidate = resolve5(project.dir, subPath);
|
|
3994
4081
|
const file = isWithinProjectRoot(project.dir, candidate) ? candidate : null;
|
|
3995
4082
|
if (!file) {
|
|
3996
4083
|
return c.text("not found", 404);
|
|
3997
4084
|
}
|
|
3998
|
-
|
|
3999
|
-
|
|
4085
|
+
recordPreviewRead(project.dir, file);
|
|
4086
|
+
const stat3 = existsSync5(file) ? statSync4(file) : null;
|
|
4087
|
+
if (!stat3?.isFile()) {
|
|
4000
4088
|
return c.text("not found", 404);
|
|
4001
4089
|
}
|
|
4002
4090
|
const contentType = getMimeType(subPath);
|
|
@@ -4007,7 +4095,7 @@ ${etag}`;
|
|
|
4007
4095
|
if (!isProxyVariantRequest(proxyParam) || !contentType.startsWith("video/") || !isAutoProxyEnabled(adapter)) {
|
|
4008
4096
|
return c.text("not found", 404);
|
|
4009
4097
|
}
|
|
4010
|
-
const facts = await probeAssetCodec(file);
|
|
4098
|
+
const facts = await probeAssetCodec(file, void 0, mediaCodecProbeCache);
|
|
4011
4099
|
const eligibility = decideMediaProxyEligibility(facts);
|
|
4012
4100
|
if (!eligibility.eligible) {
|
|
4013
4101
|
return c.text(`media proxy unavailable: ${eligibility.reason}`, 422);
|
|
@@ -4018,7 +4106,7 @@ ${etag}`;
|
|
|
4018
4106
|
return c.text("media proxy variant does not match asset", 422);
|
|
4019
4107
|
}
|
|
4020
4108
|
}
|
|
4021
|
-
const tag = settledFileTag(
|
|
4109
|
+
const tag = settledFileTag(stat3);
|
|
4022
4110
|
const etag = tag && `"${tag}${proxyEtagSalt(proxyVariant)}"`;
|
|
4023
4111
|
const cacheHeaders = isText ? { "Cache-Control": "no-store" } : { "Cache-Control": "private, no-cache", ...etag && { ETag: etag } };
|
|
4024
4112
|
if (!isText && etag) {
|
|
@@ -4087,7 +4175,7 @@ ${etag}`;
|
|
|
4087
4175
|
|
|
4088
4176
|
// src/routes/lint.ts
|
|
4089
4177
|
import { readFileSync as readFileSync9 } from "fs";
|
|
4090
|
-
import { join as
|
|
4178
|
+
import { join as join11 } from "path";
|
|
4091
4179
|
async function collectProjectFindings(adapter, projectDir) {
|
|
4092
4180
|
const findings = [];
|
|
4093
4181
|
const coveredFiles = /* @__PURE__ */ new Set();
|
|
@@ -4105,7 +4193,7 @@ async function lintUncoveredHtml(adapter, projectDir, htmlFiles, coveredFiles) {
|
|
|
4105
4193
|
const findings = [];
|
|
4106
4194
|
for (const file of htmlFiles) {
|
|
4107
4195
|
if (coveredFiles.has(file)) continue;
|
|
4108
|
-
const content = readFileSync9(
|
|
4196
|
+
const content = readFileSync9(join11(projectDir, file), "utf-8");
|
|
4109
4197
|
const result = await adapter.lint(content, { filePath: file, isSubComposition: true });
|
|
4110
4198
|
for (const finding of result?.findings ?? []) {
|
|
4111
4199
|
findings.push({ ...finding, file });
|
|
@@ -4139,7 +4227,7 @@ function registerLintRoutes(api, adapter) {
|
|
|
4139
4227
|
// src/routes/render.ts
|
|
4140
4228
|
import { streamSSE } from "hono/streaming";
|
|
4141
4229
|
import { existsSync as existsSync6, readFileSync as readFileSync10, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
4142
|
-
import { join as
|
|
4230
|
+
import { join as join12 } from "path";
|
|
4143
4231
|
import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
|
|
4144
4232
|
import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
|
|
4145
4233
|
var VALID_RESOLUTIONS = new Set(VALID_CANVAS_RESOLUTIONS);
|
|
@@ -4207,7 +4295,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4207
4295
|
const rendersDir = adapter.rendersDir(project);
|
|
4208
4296
|
if (!existsSync6(rendersDir)) mkdirSync5(rendersDir, { recursive: true });
|
|
4209
4297
|
const ext = FORMAT_EXT[format] ?? ".mp4";
|
|
4210
|
-
const outputPath =
|
|
4298
|
+
const outputPath = join12(rendersDir, `${jobId}${ext}`);
|
|
4211
4299
|
const jobState = adapter.startRender({
|
|
4212
4300
|
project,
|
|
4213
4301
|
outputPath,
|
|
@@ -4308,7 +4396,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4308
4396
|
if (state.id === jobId && state.outputPath) {
|
|
4309
4397
|
const dir = state.outputPath.replace(/\/[^/]+$/, "");
|
|
4310
4398
|
for (const ext of [".mp4", ".webm", ".mov", ".meta.json"]) {
|
|
4311
|
-
const fp =
|
|
4399
|
+
const fp = join12(dir, `${jobId}${ext}`);
|
|
4312
4400
|
if (existsSync6(fp)) unlinkSync3(fp);
|
|
4313
4401
|
}
|
|
4314
4402
|
break;
|
|
@@ -4343,10 +4431,10 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4343
4431
|
const rendersDir = adapter.rendersDir(project);
|
|
4344
4432
|
if (!existsSync6(rendersDir)) return c.json({ renders: [] });
|
|
4345
4433
|
const files = readdirSync5(rendersDir).filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov")).map((f) => {
|
|
4346
|
-
const fp =
|
|
4347
|
-
const
|
|
4434
|
+
const fp = join12(rendersDir, f);
|
|
4435
|
+
const stat3 = statSync5(fp);
|
|
4348
4436
|
const rid = f.replace(/\.(mp4|webm|mov)$/, "");
|
|
4349
|
-
const metaPath =
|
|
4437
|
+
const metaPath = join12(rendersDir, `${rid}.meta.json`);
|
|
4350
4438
|
let status = "complete";
|
|
4351
4439
|
let durationMs;
|
|
4352
4440
|
if (existsSync6(metaPath)) {
|
|
@@ -4360,8 +4448,8 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4360
4448
|
return {
|
|
4361
4449
|
id: rid,
|
|
4362
4450
|
filename: f,
|
|
4363
|
-
size:
|
|
4364
|
-
createdAt:
|
|
4451
|
+
size: stat3.size,
|
|
4452
|
+
createdAt: stat3.mtimeMs,
|
|
4365
4453
|
status,
|
|
4366
4454
|
durationMs
|
|
4367
4455
|
};
|
|
@@ -4372,7 +4460,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4372
4460
|
id: file.id,
|
|
4373
4461
|
status: file.status,
|
|
4374
4462
|
progress: 100,
|
|
4375
|
-
outputPath:
|
|
4463
|
+
outputPath: join12(rendersDir, file.filename),
|
|
4376
4464
|
createdAt: file.createdAt
|
|
4377
4465
|
});
|
|
4378
4466
|
}
|
|
@@ -4381,80 +4469,11 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4381
4469
|
});
|
|
4382
4470
|
}
|
|
4383
4471
|
|
|
4384
|
-
// src/routes/
|
|
4385
|
-
import {
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
openSync as openSync3,
|
|
4390
|
-
mkdirSync as mkdirSync6,
|
|
4391
|
-
readFileSync as readFileSync12,
|
|
4392
|
-
readdirSync as readdirSync6,
|
|
4393
|
-
renameSync as renameSync3,
|
|
4394
|
-
rmSync as rmSync4,
|
|
4395
|
-
statSync as statSync6,
|
|
4396
|
-
unlinkSync as unlinkSync4,
|
|
4397
|
-
writeFileSync as writeFileSync6
|
|
4398
|
-
} from "fs";
|
|
4399
|
-
import { join as join12 } from "path";
|
|
4400
|
-
import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
|
|
4401
|
-
|
|
4402
|
-
// src/helpers/compositionInputs.ts
|
|
4403
|
-
import { readFileSync as readFileSync11 } from "fs";
|
|
4404
|
-
var ROOT_COMPOSITION = "index.html";
|
|
4405
|
-
var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
|
|
4406
|
-
function projectReader(projectDir) {
|
|
4407
|
-
const sources = /* @__PURE__ */ new Map();
|
|
4408
|
-
return (compPath) => {
|
|
4409
|
-
if (!sources.has(compPath)) {
|
|
4410
|
-
const file = resolveWithinProject2(projectDir, compPath);
|
|
4411
|
-
let source = null;
|
|
4412
|
-
try {
|
|
4413
|
-
source = file ? readFileSync11(file, "utf-8") : null;
|
|
4414
|
-
} catch {
|
|
4415
|
-
}
|
|
4416
|
-
sources.set(compPath, source);
|
|
4417
|
-
}
|
|
4418
|
-
return sources.get(compPath) ?? null;
|
|
4419
|
-
};
|
|
4420
|
-
}
|
|
4421
|
-
function normalizeSource(src) {
|
|
4422
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) return null;
|
|
4423
|
-
const path = src.split(/[?#]/)[0]?.replace(/^\.?\//, "") ?? "";
|
|
4424
|
-
return path || null;
|
|
4425
|
-
}
|
|
4426
|
-
function closureOf(read, compPath) {
|
|
4427
|
-
const closure = /* @__PURE__ */ new Set();
|
|
4428
|
-
const visit = (path) => {
|
|
4429
|
-
if (closure.has(path)) return;
|
|
4430
|
-
closure.add(path);
|
|
4431
|
-
for (const match of (read(path) ?? "").matchAll(COMPOSITION_SRC)) {
|
|
4432
|
-
const mounted = normalizeSource(match[2] ?? "");
|
|
4433
|
-
if (mounted) visit(mounted);
|
|
4434
|
-
}
|
|
4435
|
-
};
|
|
4436
|
-
visit(compPath);
|
|
4437
|
-
return closure;
|
|
4438
|
-
}
|
|
4439
|
-
var inputSignatures = /* @__PURE__ */ new Map();
|
|
4440
|
-
function compositionInputSignature(projectDir, compPath, projectSignature) {
|
|
4441
|
-
const key = `${projectDir}\0${compPath}`;
|
|
4442
|
-
const known = inputSignatures.get(key);
|
|
4443
|
-
if (known?.projectSignature === projectSignature) return known.inputSignature;
|
|
4444
|
-
const read = projectReader(projectDir);
|
|
4445
|
-
const inputs = closureOf(read, normalizeSource(compPath) ?? compPath).add(ROOT_COMPOSITION);
|
|
4446
|
-
const siblings = [...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path));
|
|
4447
|
-
const inputSignature = createProjectSignature(projectDir, new Set(siblings));
|
|
4448
|
-
inputSignatures.set(key, { projectSignature, inputSignature });
|
|
4449
|
-
return inputSignature;
|
|
4450
|
-
}
|
|
4451
|
-
function compositionsAffectedBy(projectDir, changedPath) {
|
|
4452
|
-
const changed = changedPath.replace(/\\/g, "/");
|
|
4453
|
-
const read = projectReader(projectDir);
|
|
4454
|
-
const mounted = closureOf(read, ROOT_COMPOSITION);
|
|
4455
|
-
if (changed === ROOT_COMPOSITION || !mounted.has(changed)) return null;
|
|
4456
|
-
return [...mounted].filter((path) => closureOf(read, path).has(changed));
|
|
4457
|
-
}
|
|
4472
|
+
// src/routes/imageThumbnail.ts
|
|
4473
|
+
import { HTTPException } from "hono/http-exception";
|
|
4474
|
+
import { open, stat } from "fs/promises";
|
|
4475
|
+
import { createHash as createHash5 } from "crypto";
|
|
4476
|
+
import sharp from "sharp";
|
|
4458
4477
|
|
|
4459
4478
|
// src/routes/thumbnailGenerationCoordinator.ts
|
|
4460
4479
|
var ThumbnailGenerationCoordinator = class {
|
|
@@ -4473,10 +4492,10 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4473
4492
|
if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
4474
4493
|
let entry = this.entries.get(key);
|
|
4475
4494
|
if (!entry) {
|
|
4476
|
-
let
|
|
4495
|
+
let resolve9;
|
|
4477
4496
|
let reject;
|
|
4478
4497
|
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
4479
|
-
|
|
4498
|
+
resolve9 = resolvePromise;
|
|
4480
4499
|
reject = rejectPromise;
|
|
4481
4500
|
});
|
|
4482
4501
|
entry = {
|
|
@@ -4486,7 +4505,7 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4486
4505
|
state: "queued",
|
|
4487
4506
|
work,
|
|
4488
4507
|
promise,
|
|
4489
|
-
resolve:
|
|
4508
|
+
resolve: resolve9,
|
|
4490
4509
|
reject
|
|
4491
4510
|
};
|
|
4492
4511
|
this.entries.set(key, entry);
|
|
@@ -4500,7 +4519,7 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4500
4519
|
return /* @__PURE__ */ new Set([...this.entries.keys(), ...[...this.activeEntries].map((entry) => entry.key)]);
|
|
4501
4520
|
}
|
|
4502
4521
|
lease(entry, signal) {
|
|
4503
|
-
return new Promise((
|
|
4522
|
+
return new Promise((resolve9, reject) => {
|
|
4504
4523
|
let released = false;
|
|
4505
4524
|
const release = () => {
|
|
4506
4525
|
if (released) return;
|
|
@@ -4524,7 +4543,7 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4524
4543
|
entry.promise.then(
|
|
4525
4544
|
(value) => {
|
|
4526
4545
|
release();
|
|
4527
|
-
|
|
4546
|
+
resolve9(value);
|
|
4528
4547
|
},
|
|
4529
4548
|
(reason) => {
|
|
4530
4549
|
release();
|
|
@@ -4559,6 +4578,171 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4559
4578
|
};
|
|
4560
4579
|
var thumbnailGenerationCoordinator = new ThumbnailGenerationCoordinator(1);
|
|
4561
4580
|
|
|
4581
|
+
// src/routes/imageThumbnail.ts
|
|
4582
|
+
var MAX_SOURCE_BYTES = 64 * 1024 * 1024;
|
|
4583
|
+
var MAX_CACHE_BYTES = 8 * 1024 * 1024;
|
|
4584
|
+
async function generateThumbnail(path, signal) {
|
|
4585
|
+
const file = await open(path, "r");
|
|
4586
|
+
try {
|
|
4587
|
+
const info = await file.stat();
|
|
4588
|
+
if (!info.isFile() || info.size > MAX_SOURCE_BYTES) throw new Error("Unsupported image");
|
|
4589
|
+
const source = Buffer.alloc(info.size);
|
|
4590
|
+
let offset = 0;
|
|
4591
|
+
while (offset < source.length) {
|
|
4592
|
+
signal.throwIfAborted();
|
|
4593
|
+
const { bytesRead } = await file.read(source, offset, source.length - offset, offset);
|
|
4594
|
+
if (!bytesRead) break;
|
|
4595
|
+
offset += bytesRead;
|
|
4596
|
+
}
|
|
4597
|
+
if (source[0] !== 255 || source[1] !== 216) throw new Error("Unsupported image");
|
|
4598
|
+
const image = sharp(source.subarray(0, offset), { limitInputPixels: 4e7 });
|
|
4599
|
+
if ((await image.metadata()).format !== "jpeg") throw new Error("Unsupported image");
|
|
4600
|
+
signal.throwIfAborted();
|
|
4601
|
+
const result = await image.rotate().resize(240, 135, { fit: "inside", withoutEnlargement: true }).jpeg({ quality: 80 }).toBuffer();
|
|
4602
|
+
signal.throwIfAborted();
|
|
4603
|
+
return result;
|
|
4604
|
+
} finally {
|
|
4605
|
+
await file.close();
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
async function resolveSource(adapter, id, url) {
|
|
4609
|
+
const project = await adapter.resolveProject(id);
|
|
4610
|
+
if (!project) throw new HTTPException(404);
|
|
4611
|
+
let relative7;
|
|
4612
|
+
try {
|
|
4613
|
+
relative7 = requestSubPath(url, "projects/:id/image-thumbnail");
|
|
4614
|
+
} catch {
|
|
4615
|
+
throw new HTTPException(400);
|
|
4616
|
+
}
|
|
4617
|
+
if (!/\.jpe?g$/i.test(relative7)) throw new HTTPException(415);
|
|
4618
|
+
const path = pinWithinProject(project.dir, relative7);
|
|
4619
|
+
if (!path) throw new HTTPException(404);
|
|
4620
|
+
const info = await stat(path).catch(() => {
|
|
4621
|
+
throw new HTTPException(404);
|
|
4622
|
+
});
|
|
4623
|
+
if (!info.isFile()) throw new HTTPException(404);
|
|
4624
|
+
if (info.size > MAX_SOURCE_BYTES) throw new HTTPException(413);
|
|
4625
|
+
return { path, info };
|
|
4626
|
+
}
|
|
4627
|
+
function registerImageThumbnailRoutes(api, adapter) {
|
|
4628
|
+
const coordinator = new ThumbnailGenerationCoordinator(2);
|
|
4629
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4630
|
+
let cacheBytes = 0;
|
|
4631
|
+
const remember = (key, image) => {
|
|
4632
|
+
cacheBytes -= cache.get(key)?.byteLength ?? 0;
|
|
4633
|
+
cache.delete(key);
|
|
4634
|
+
cache.set(key, image);
|
|
4635
|
+
cacheBytes += image.byteLength;
|
|
4636
|
+
while (cacheBytes > MAX_CACHE_BYTES || cache.size > 128) {
|
|
4637
|
+
const oldest = cache.keys().next().value;
|
|
4638
|
+
if (oldest === void 0) break;
|
|
4639
|
+
cacheBytes -= cache.get(oldest)?.byteLength ?? 0;
|
|
4640
|
+
cache.delete(oldest);
|
|
4641
|
+
}
|
|
4642
|
+
};
|
|
4643
|
+
api.get("/projects/:id/image-thumbnail/*", async (c) => {
|
|
4644
|
+
try {
|
|
4645
|
+
const { path, info } = await resolveSource(adapter, c.req.param("id"), c.req.url);
|
|
4646
|
+
const key = createHash5("sha256").update(JSON.stringify([path, info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs])).digest("hex");
|
|
4647
|
+
const etag = `"${key}"`;
|
|
4648
|
+
c.header("ETag", etag);
|
|
4649
|
+
c.header("Cache-Control", "private, no-cache");
|
|
4650
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
4651
|
+
if (c.req.header("If-None-Match") === etag) return c.body(null, 304);
|
|
4652
|
+
let result = cache.get(key);
|
|
4653
|
+
if (result) remember(key, result);
|
|
4654
|
+
else {
|
|
4655
|
+
if (coordinator.protectedKeys().size >= 32) return c.text("thumbnail queue full", 429);
|
|
4656
|
+
result = await coordinator.acquire(key, c.req.raw.signal, async (signal) => {
|
|
4657
|
+
const image = await generateThumbnail(path, signal);
|
|
4658
|
+
remember(key, image);
|
|
4659
|
+
return image;
|
|
4660
|
+
}) ?? void 0;
|
|
4661
|
+
}
|
|
4662
|
+
if (!result) return c.text("image unavailable", 422);
|
|
4663
|
+
c.header("Content-Type", "image/jpeg");
|
|
4664
|
+
return c.body(new Uint8Array(result));
|
|
4665
|
+
} catch (error) {
|
|
4666
|
+
if (error instanceof HTTPException) return error.getResponse();
|
|
4667
|
+
return c.text("image unavailable", 422);
|
|
4668
|
+
}
|
|
4669
|
+
});
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
// src/routes/thumbnail.ts
|
|
4673
|
+
import {
|
|
4674
|
+
closeSync as closeSync3,
|
|
4675
|
+
fstatSync as fstatSync3,
|
|
4676
|
+
openSync as openSync3,
|
|
4677
|
+
mkdirSync as mkdirSync6,
|
|
4678
|
+
readFileSync as readFileSync12,
|
|
4679
|
+
readdirSync as readdirSync6,
|
|
4680
|
+
renameSync as renameSync3,
|
|
4681
|
+
rmSync as rmSync4,
|
|
4682
|
+
statSync as statSync6,
|
|
4683
|
+
unlinkSync as unlinkSync4,
|
|
4684
|
+
writeFileSync as writeFileSync6
|
|
4685
|
+
} from "fs";
|
|
4686
|
+
import { join as join13 } from "path";
|
|
4687
|
+
import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
|
|
4688
|
+
|
|
4689
|
+
// src/helpers/compositionInputs.ts
|
|
4690
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
4691
|
+
var ROOT_COMPOSITION = "index.html";
|
|
4692
|
+
var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
|
|
4693
|
+
function projectReader(projectDir) {
|
|
4694
|
+
const sources = /* @__PURE__ */ new Map();
|
|
4695
|
+
return (compPath) => {
|
|
4696
|
+
if (!sources.has(compPath)) {
|
|
4697
|
+
const file = resolveWithinProject2(projectDir, compPath);
|
|
4698
|
+
let source = null;
|
|
4699
|
+
try {
|
|
4700
|
+
source = file ? readFileSync11(file, "utf-8") : null;
|
|
4701
|
+
} catch {
|
|
4702
|
+
}
|
|
4703
|
+
sources.set(compPath, source);
|
|
4704
|
+
}
|
|
4705
|
+
return sources.get(compPath) ?? null;
|
|
4706
|
+
};
|
|
4707
|
+
}
|
|
4708
|
+
function normalizeSource(src) {
|
|
4709
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) return null;
|
|
4710
|
+
const path = src.split(/[?#]/)[0]?.replace(/^\.?\//, "") ?? "";
|
|
4711
|
+
return path || null;
|
|
4712
|
+
}
|
|
4713
|
+
function closureOf(read, compPath) {
|
|
4714
|
+
const closure = /* @__PURE__ */ new Set();
|
|
4715
|
+
const visit = (path) => {
|
|
4716
|
+
if (closure.has(path)) return;
|
|
4717
|
+
closure.add(path);
|
|
4718
|
+
for (const match of (read(path) ?? "").matchAll(COMPOSITION_SRC)) {
|
|
4719
|
+
const mounted = normalizeSource(match[2] ?? "");
|
|
4720
|
+
if (mounted) visit(mounted);
|
|
4721
|
+
}
|
|
4722
|
+
};
|
|
4723
|
+
visit(compPath);
|
|
4724
|
+
return closure;
|
|
4725
|
+
}
|
|
4726
|
+
var inputSignatures = /* @__PURE__ */ new Map();
|
|
4727
|
+
function compositionInputSignature(projectDir, compPath, projectSignature) {
|
|
4728
|
+
const key = `${projectDir}\0${compPath}`;
|
|
4729
|
+
const known = inputSignatures.get(key);
|
|
4730
|
+
if (known?.projectSignature === projectSignature) return known.inputSignature;
|
|
4731
|
+
const read = projectReader(projectDir);
|
|
4732
|
+
const inputs = closureOf(read, normalizeSource(compPath) ?? compPath).add(ROOT_COMPOSITION);
|
|
4733
|
+
const siblings = [...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path));
|
|
4734
|
+
const inputSignature = createProjectSignature(projectDir, new Set(siblings));
|
|
4735
|
+
inputSignatures.set(key, { projectSignature, inputSignature });
|
|
4736
|
+
return inputSignature;
|
|
4737
|
+
}
|
|
4738
|
+
function compositionsAffectedBy(projectDir, changedPath) {
|
|
4739
|
+
const changed = changedPath.replace(/\\/g, "/");
|
|
4740
|
+
const read = projectReader(projectDir);
|
|
4741
|
+
const mounted = closureOf(read, ROOT_COMPOSITION);
|
|
4742
|
+
if (changed === ROOT_COMPOSITION || !mounted.has(changed)) return null;
|
|
4743
|
+
return [...mounted].filter((path) => closureOf(read, path).has(changed));
|
|
4744
|
+
}
|
|
4745
|
+
|
|
4562
4746
|
// src/routes/thumbnail.ts
|
|
4563
4747
|
var THUMBNAIL_CACHE_VERSION = "v4";
|
|
4564
4748
|
var THUMBNAIL_MAX_OUTPUT_WIDTH = 240;
|
|
@@ -4567,10 +4751,16 @@ var THUMBNAIL_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
|
4567
4751
|
var THUMBNAIL_CACHE_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
4568
4752
|
var prunedCacheDirs = /* @__PURE__ */ new Set();
|
|
4569
4753
|
function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
|
|
4570
|
-
|
|
4571
|
-
|
|
4754
|
+
let entries;
|
|
4755
|
+
try {
|
|
4756
|
+
entries = readdirSync6(cacheDir, { withFileTypes: true });
|
|
4757
|
+
} catch (err) {
|
|
4758
|
+
if (err.code === "ENOENT") return;
|
|
4759
|
+
throw err;
|
|
4760
|
+
}
|
|
4761
|
+
const files = entries.flatMap((entry) => {
|
|
4572
4762
|
if (!entry.isFile()) return [];
|
|
4573
|
-
const path =
|
|
4763
|
+
const path = join13(cacheDir, entry.name);
|
|
4574
4764
|
try {
|
|
4575
4765
|
const stats = statSync6(path);
|
|
4576
4766
|
return [{ path, bytes: stats.size, mtimeMs: stats.mtimeMs }];
|
|
@@ -4606,22 +4796,30 @@ function writeThumbnailAtomically(path, buffer) {
|
|
|
4606
4796
|
rmSync4(temporaryPath, { force: true });
|
|
4607
4797
|
}
|
|
4608
4798
|
}
|
|
4609
|
-
function
|
|
4799
|
+
function readFileOnce(file) {
|
|
4610
4800
|
let fd;
|
|
4611
4801
|
try {
|
|
4612
4802
|
fd = openSync3(file, "r");
|
|
4613
4803
|
} catch (err) {
|
|
4614
|
-
|
|
4804
|
+
const code = err.code;
|
|
4805
|
+
if (code === "ENOENT") return "missing";
|
|
4806
|
+
if (code === "ENOTDIR") return "not-a-file";
|
|
4615
4807
|
throw err;
|
|
4616
4808
|
}
|
|
4617
4809
|
try {
|
|
4618
|
-
const
|
|
4619
|
-
if (!
|
|
4620
|
-
return {
|
|
4810
|
+
const stat3 = fstatSync3(fd);
|
|
4811
|
+
if (!stat3.isFile()) return "not-a-file";
|
|
4812
|
+
return { data: readFileSync12(fd), mtimeMs: stat3.mtimeMs };
|
|
4621
4813
|
} finally {
|
|
4622
4814
|
closeSync3(fd);
|
|
4623
4815
|
}
|
|
4624
4816
|
}
|
|
4817
|
+
function manifestKey(file) {
|
|
4818
|
+
const manifest = readFileOnce(file);
|
|
4819
|
+
if (typeof manifest === "string") return { key: "", mtimeMs: -Infinity };
|
|
4820
|
+
const hash = createHash6("sha1").update(manifest.data).digest("hex").slice(0, 16);
|
|
4821
|
+
return { key: `_${hash}`, mtimeMs: Math.round(manifest.mtimeMs) };
|
|
4822
|
+
}
|
|
4625
4823
|
function registerThumbnailRoutes(api, adapter) {
|
|
4626
4824
|
api.get("/projects/:id/thumbnail/*", async (c) => {
|
|
4627
4825
|
if (!adapter.generateThumbnail) {
|
|
@@ -4633,7 +4831,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4633
4831
|
let compPath = requestSubPath(c.req.url, "projects/:id/thumbnail");
|
|
4634
4832
|
if (compPath && !compPath.includes(".")) compPath += ".html";
|
|
4635
4833
|
const htmlFile = resolveWithinProject2(project.dir, compPath);
|
|
4636
|
-
const source = htmlFile ?
|
|
4834
|
+
const source = htmlFile ? readFileOnce(htmlFile) : "not-a-file";
|
|
4637
4835
|
if (source === "not-a-file") return c.json({ error: "not found" }, 404);
|
|
4638
4836
|
const inputSignature = compositionInputSignature(project.dir, compPath, projectSignature);
|
|
4639
4837
|
const url = new URL(c.req.url, `http://${c.req.header("host") || "localhost"}`);
|
|
@@ -4655,8 +4853,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4655
4853
|
let sourceMtime = 0;
|
|
4656
4854
|
let sourceKey = "";
|
|
4657
4855
|
if (source !== "missing") {
|
|
4658
|
-
const
|
|
4659
|
-
sourceKey = `_${
|
|
4856
|
+
const html = source.data.toString("utf-8");
|
|
4857
|
+
sourceKey = `_${createHash6("sha1").update(html).digest("hex").slice(0, 16)}`;
|
|
4660
4858
|
sourceMtime = Math.round(source.mtimeMs);
|
|
4661
4859
|
if (!vpWidth) {
|
|
4662
4860
|
const wMatch = html.match(/data-width=["'](\d+)["']/);
|
|
@@ -4665,31 +4863,20 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4665
4863
|
if (hMatch?.[1]) compH = parseInt(hMatch[1]);
|
|
4666
4864
|
}
|
|
4667
4865
|
}
|
|
4668
|
-
const
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
const manualEditsContent = readFileSync12(manualEditsFile, "utf-8");
|
|
4672
|
-
manualEditsKey = `_${createHash5("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
|
|
4673
|
-
sourceMtime = Math.max(sourceMtime, Math.round(statSync6(manualEditsFile).mtimeMs));
|
|
4674
|
-
}
|
|
4675
|
-
const motionFile = join12(project.dir, STUDIO_MOTION_PATH);
|
|
4676
|
-
let motionKey = "";
|
|
4677
|
-
if (existsSync7(motionFile)) {
|
|
4678
|
-
const motionContent = readFileSync12(motionFile, "utf-8");
|
|
4679
|
-
motionKey = `_${createHash5("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
|
|
4680
|
-
sourceMtime = Math.max(sourceMtime, Math.round(statSync6(motionFile).mtimeMs));
|
|
4681
|
-
}
|
|
4866
|
+
const manualEdits = manifestKey(join13(project.dir, STUDIO_MANUAL_EDITS_PATH));
|
|
4867
|
+
const motion = manifestKey(join13(project.dir, STUDIO_MOTION_PATH));
|
|
4868
|
+
sourceMtime = Math.max(sourceMtime, manualEdits.mtimeMs, motion.mtimeMs);
|
|
4682
4869
|
const projectUrl = `http://${c.req.header("host")}/api/projects/${encodeURIComponent(project.id)}`;
|
|
4683
4870
|
const previewUrl = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
|
|
4684
|
-
const cacheDir =
|
|
4871
|
+
const cacheDir = join13(project.dir, ".thumbnails");
|
|
4685
4872
|
const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
|
|
4686
4873
|
const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
|
|
4687
|
-
const inputSignatureKey = `_${
|
|
4874
|
+
const inputSignatureKey = `_${createHash6("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
|
|
4688
4875
|
const outputScale = outputMode === "source" ? 1 : Math.min(1, THUMBNAIL_MAX_OUTPUT_WIDTH / compW, THUMBNAIL_MAX_OUTPUT_HEIGHT / compH);
|
|
4689
4876
|
const outputWidth = Math.max(1, Math.round(compW * outputScale));
|
|
4690
4877
|
const outputHeight = Math.max(1, Math.round(compH * outputScale));
|
|
4691
|
-
const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${inputSignatureKey}${
|
|
4692
|
-
const cachePath =
|
|
4878
|
+
const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${inputSignatureKey}${manualEdits.key}${motion.key}${sourceKey}_${format}_${outputMode}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${outputWidth}x${outputHeight}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
|
|
4879
|
+
const cachePath = join13(cacheDir, cacheKey);
|
|
4693
4880
|
if (!prunedCacheDirs.has(cacheDir)) {
|
|
4694
4881
|
prunedCacheDirs.add(cacheDir);
|
|
4695
4882
|
pruneThumbnailCache(
|
|
@@ -4697,8 +4884,9 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4697
4884
|
/* @__PURE__ */ new Set([...thumbnailGenerationCoordinator.protectedKeys(), cachePath])
|
|
4698
4885
|
);
|
|
4699
4886
|
}
|
|
4700
|
-
|
|
4701
|
-
|
|
4887
|
+
const cached = readFileOnce(cachePath);
|
|
4888
|
+
if (typeof cached === "object") {
|
|
4889
|
+
return new Response(new Uint8Array(cached.data), {
|
|
4702
4890
|
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
4703
4891
|
});
|
|
4704
4892
|
}
|
|
@@ -4728,7 +4916,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4728
4916
|
if (afterGeneration?.project.dir !== project.dir || !inputsUnchanged(afterGeneration.signature) || !inputsUnchanged(createProjectSignature(project.dir))) {
|
|
4729
4917
|
return generated;
|
|
4730
4918
|
}
|
|
4731
|
-
|
|
4919
|
+
mkdirSync6(cacheDir, { recursive: true });
|
|
4732
4920
|
writeThumbnailAtomically(cachePath, generated);
|
|
4733
4921
|
return generated;
|
|
4734
4922
|
}
|
|
@@ -4754,24 +4942,24 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4754
4942
|
}
|
|
4755
4943
|
|
|
4756
4944
|
// src/routes/waveform.ts
|
|
4757
|
-
import { existsSync as
|
|
4758
|
-
import { join as
|
|
4945
|
+
import { existsSync as existsSync7, readFileSync as readFileSync13, statSync as statSync7 } from "fs";
|
|
4946
|
+
import { join as join14, resolve as resolve6 } from "path";
|
|
4759
4947
|
import { isWithinProjectRoot as isWithinProjectRoot2 } from "@hyperframes/parsers/asset-resolution";
|
|
4760
4948
|
function registerWaveformRoutes(api, adapter) {
|
|
4761
4949
|
api.get("/projects/:id/waveform/*", async (c) => {
|
|
4762
4950
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
4763
4951
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
4764
4952
|
const assetPath = requestSubPath(c.req.url, "projects/:id/waveform");
|
|
4765
|
-
const audioPath =
|
|
4953
|
+
const audioPath = resolve6(project.dir, assetPath);
|
|
4766
4954
|
if (!isWithinProjectRoot2(project.dir, audioPath)) {
|
|
4767
4955
|
return c.json({ error: "file not found" }, 404);
|
|
4768
4956
|
}
|
|
4769
4957
|
const stats = statSync7(audioPath, { throwIfNoEntry: false });
|
|
4770
4958
|
if (!stats) return c.json({ error: "file not found" }, 404);
|
|
4771
|
-
const cacheDir =
|
|
4772
|
-
const cachePath =
|
|
4959
|
+
const cacheDir = join14(project.dir, ".waveform-cache");
|
|
4960
|
+
const cachePath = join14(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
4773
4961
|
try {
|
|
4774
|
-
if (isWaveformCacheDirectory(cacheDir) &&
|
|
4962
|
+
if (isWaveformCacheDirectory(cacheDir) && existsSync7(cachePath)) {
|
|
4775
4963
|
const peaks2 = JSON.parse(readFileSync13(cachePath, "utf-8"));
|
|
4776
4964
|
return c.json({ peaks: peaks2 });
|
|
4777
4965
|
}
|
|
@@ -4904,12 +5092,12 @@ function registerFontRoutes(api) {
|
|
|
4904
5092
|
return c.json({ error: "font file not accessible" }, 404);
|
|
4905
5093
|
}
|
|
4906
5094
|
try {
|
|
4907
|
-
const
|
|
4908
|
-
if (
|
|
5095
|
+
const stat3 = fstatSync4(fd);
|
|
5096
|
+
if (stat3.size > SYSTEM_FONT_SIZE_LIMIT) {
|
|
4909
5097
|
return c.json({ error: "font file too large" }, 413);
|
|
4910
5098
|
}
|
|
4911
|
-
const buffer = Buffer.alloc(
|
|
4912
|
-
readSync(fd, buffer, 0,
|
|
5099
|
+
const buffer = Buffer.alloc(stat3.size);
|
|
5100
|
+
readSync(fd, buffer, 0, stat3.size, 0);
|
|
4913
5101
|
const mimeType = located.format === "otf" ? "font/otf" : located.format === "woff2" ? "font/woff2" : located.format === "woff" ? "font/woff" : located.format === "ttc" ? "font/collection" : "font/ttf";
|
|
4914
5102
|
const fileName = `${family.replace(/[^a-zA-Z0-9 -]/g, "")}.${located.format}`;
|
|
4915
5103
|
return new Response(buffer, {
|
|
@@ -4937,7 +5125,7 @@ function registerRegistryRoutes(api, adapter) {
|
|
|
4937
5125
|
});
|
|
4938
5126
|
api.post("/projects/:id/registry/install", async (c) => {
|
|
4939
5127
|
if (!adapter.installRegistryBlock) {
|
|
4940
|
-
return c.json({ error: "
|
|
5128
|
+
return c.json({ error: "Installing catalog items needs hyperframes preview" }, 501);
|
|
4941
5129
|
}
|
|
4942
5130
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
4943
5131
|
if (!project) return c.json({ error: "Project not found" }, 404);
|
|
@@ -5055,8 +5243,8 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
5055
5243
|
|
|
5056
5244
|
// src/routes/media.ts
|
|
5057
5245
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
5058
|
-
import { existsSync as
|
|
5059
|
-
import { basename as basename3, dirname as
|
|
5246
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
|
|
5247
|
+
import { basename as basename3, dirname as dirname7, extname as extname2, join as join15 } from "path";
|
|
5060
5248
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5061
5249
|
".mp4",
|
|
5062
5250
|
".mov",
|
|
@@ -5093,7 +5281,7 @@ function uniqueAssetPath(projectDir, assetPath) {
|
|
|
5093
5281
|
const ext = extname2(assetPath);
|
|
5094
5282
|
const withoutExt = assetPath.slice(0, -ext.length);
|
|
5095
5283
|
let candidate = assetPath;
|
|
5096
|
-
for (let index = 2;
|
|
5284
|
+
for (let index = 2; existsSync8(join15(projectDir, candidate)); index++) {
|
|
5097
5285
|
candidate = `${withoutExt}-${index}${ext}`;
|
|
5098
5286
|
}
|
|
5099
5287
|
return candidate;
|
|
@@ -5144,7 +5332,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5144
5332
|
}
|
|
5145
5333
|
const filePath = resolveWithinProject2(project.dir, assetPath);
|
|
5146
5334
|
if (!filePath) return c.json({ error: "forbidden" }, 403);
|
|
5147
|
-
if (!
|
|
5335
|
+
if (!existsSync8(filePath)) return c.json({ error: "media not found" }, 404);
|
|
5148
5336
|
return c.json({ path: assetPath, metadata: await readMediaMetadata(filePath) });
|
|
5149
5337
|
});
|
|
5150
5338
|
api.post(
|
|
@@ -5166,7 +5354,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5166
5354
|
}
|
|
5167
5355
|
const inputPath = resolveWithinProject2(project.dir, inputAssetPath);
|
|
5168
5356
|
if (!inputPath) return c.json({ error: "forbidden" }, 403);
|
|
5169
|
-
if (!
|
|
5357
|
+
if (!existsSync8(inputPath)) return c.json({ error: "input media not found" }, 404);
|
|
5170
5358
|
const inputIsVideo = isVideoPath(inputAssetPath);
|
|
5171
5359
|
const inputIsImage = isImagePath(inputAssetPath);
|
|
5172
5360
|
if (!inputIsVideo && !inputIsImage) {
|
|
@@ -5200,8 +5388,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5200
5388
|
return c.json({ error: "forbidden" }, 403);
|
|
5201
5389
|
}
|
|
5202
5390
|
}
|
|
5203
|
-
mkdirSync7(
|
|
5204
|
-
if (backgroundOutputPath) mkdirSync7(
|
|
5391
|
+
mkdirSync7(dirname7(outputPath), { recursive: true });
|
|
5392
|
+
if (backgroundOutputPath) mkdirSync7(dirname7(backgroundOutputPath), { recursive: true });
|
|
5205
5393
|
const jobId = makeJobId(project.id, mediaJobs);
|
|
5206
5394
|
const state = adapter.startBackgroundRemoval({
|
|
5207
5395
|
project,
|
|
@@ -5258,12 +5446,12 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5258
5446
|
}
|
|
5259
5447
|
|
|
5260
5448
|
// src/routes/globalAssets.ts
|
|
5261
|
-
import { existsSync as
|
|
5449
|
+
import { existsSync as existsSync9, readFileSync as readFileSync14 } from "fs";
|
|
5262
5450
|
import { homedir } from "os";
|
|
5263
|
-
import { join as
|
|
5451
|
+
import { join as join16 } from "path";
|
|
5264
5452
|
function readGlobalAssets(home = homedir()) {
|
|
5265
|
-
const manifestPath =
|
|
5266
|
-
if (!
|
|
5453
|
+
const manifestPath = join16(home, ".media", "manifest.jsonl");
|
|
5454
|
+
if (!existsSync9(manifestPath)) return [];
|
|
5267
5455
|
const out = [];
|
|
5268
5456
|
for (const line of readFileSync14(manifestPath, "utf8").split("\n")) {
|
|
5269
5457
|
if (!line.trim()) continue;
|
|
@@ -5286,7 +5474,7 @@ function registerGlobalAssetRoutes(api) {
|
|
|
5286
5474
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
5287
5475
|
import { readdir as readdir2, rm as rm2, rmdir, writeFile } from "fs/promises";
|
|
5288
5476
|
import {
|
|
5289
|
-
existsSync as
|
|
5477
|
+
existsSync as existsSync11,
|
|
5290
5478
|
lstatSync as lstatSync4,
|
|
5291
5479
|
statSync as statSync9,
|
|
5292
5480
|
mkdirSync as mkdirSync11,
|
|
@@ -5296,21 +5484,21 @@ import {
|
|
|
5296
5484
|
rmSync as rmSync6
|
|
5297
5485
|
} from "fs";
|
|
5298
5486
|
import { homedir as homedir2 } from "os";
|
|
5299
|
-
import { dirname as
|
|
5487
|
+
import { dirname as dirname10, isAbsolute as isAbsolute3, join as join20, relative as relative6, resolve as resolve8, sep as sep4 } from "path";
|
|
5300
5488
|
|
|
5301
5489
|
// src/history/blobStore.ts
|
|
5302
|
-
import { createHash as
|
|
5490
|
+
import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
|
|
5303
5491
|
import { constants as constants3, createReadStream as createReadStream2, renameSync as renameSync4 } from "fs";
|
|
5304
|
-
import { copyFile, mkdir, readdir, readFile as readFile2, rename, rm, stat } from "fs/promises";
|
|
5305
|
-
import { dirname as
|
|
5492
|
+
import { copyFile, mkdir, readdir, readFile as readFile2, rename, rm, stat as stat2 } from "fs/promises";
|
|
5493
|
+
import { dirname as dirname8, join as join17 } from "path";
|
|
5306
5494
|
var BLOB_HASH = /^[0-9a-f]{64}$/;
|
|
5307
5495
|
async function hashFile(path) {
|
|
5308
|
-
const hash =
|
|
5496
|
+
const hash = createHash7("sha256");
|
|
5309
5497
|
for await (const chunk of createReadStream2(path)) hash.update(chunk);
|
|
5310
5498
|
return hash.digest("hex");
|
|
5311
5499
|
}
|
|
5312
5500
|
async function cloneOrCopy(from, to, beforeReplace) {
|
|
5313
|
-
await mkdir(
|
|
5501
|
+
await mkdir(dirname8(to), { recursive: true });
|
|
5314
5502
|
const temp = `${to}.${randomUUID5()}.tmp`;
|
|
5315
5503
|
try {
|
|
5316
5504
|
await copyFile(from, temp, constants3.COPYFILE_FICLONE);
|
|
@@ -5325,16 +5513,16 @@ async function openBlobStore(dir) {
|
|
|
5325
5513
|
const sizes = /* @__PURE__ */ new Map();
|
|
5326
5514
|
await mkdir(dir, { recursive: true });
|
|
5327
5515
|
for (const shard of await readdir(dir))
|
|
5328
|
-
for (const hash of await readdir(
|
|
5329
|
-
sizes.set(hash, (await
|
|
5516
|
+
for (const hash of await readdir(join17(dir, shard)).catch(() => []))
|
|
5517
|
+
sizes.set(hash, (await stat2(join17(dir, shard, hash))).size);
|
|
5330
5518
|
const pathOf = (hash) => {
|
|
5331
5519
|
if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
|
|
5332
|
-
return
|
|
5520
|
+
return join17(dir, hash.slice(0, 2), hash);
|
|
5333
5521
|
};
|
|
5334
5522
|
let total = [...sizes.values()].reduce((sum, size) => sum + size, 0);
|
|
5335
5523
|
return {
|
|
5336
5524
|
async put(absPath) {
|
|
5337
|
-
const temp =
|
|
5525
|
+
const temp = join17(dir, `incoming-${randomUUID5()}`);
|
|
5338
5526
|
await mkdir(dir, { recursive: true });
|
|
5339
5527
|
await copyFile(absPath, temp, constants3.COPYFILE_FICLONE);
|
|
5340
5528
|
const hash = await hashFile(temp);
|
|
@@ -5342,8 +5530,8 @@ async function openBlobStore(dir) {
|
|
|
5342
5530
|
await rm(temp, { force: true });
|
|
5343
5531
|
return hash;
|
|
5344
5532
|
}
|
|
5345
|
-
await mkdir(
|
|
5346
|
-
const size = (await
|
|
5533
|
+
await mkdir(dirname8(pathOf(hash)), { recursive: true });
|
|
5534
|
+
const size = (await stat2(temp)).size;
|
|
5347
5535
|
await rename(temp, pathOf(hash));
|
|
5348
5536
|
sizes.set(hash, size);
|
|
5349
5537
|
total += size;
|
|
@@ -5367,13 +5555,13 @@ async function openBlobStore(dir) {
|
|
|
5367
5555
|
|
|
5368
5556
|
// src/history/historyId.ts
|
|
5369
5557
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
5370
|
-
import { existsSync as
|
|
5371
|
-
import { join as
|
|
5372
|
-
var ID_PATH =
|
|
5558
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync15, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
5559
|
+
import { join as join18, resolve as resolve7 } from "path";
|
|
5560
|
+
var ID_PATH = join18(".hyperframes", "history-id");
|
|
5373
5561
|
var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
5374
5562
|
function readId(projectDir) {
|
|
5375
5563
|
try {
|
|
5376
|
-
const id = readFileSync15(
|
|
5564
|
+
const id = readFileSync15(join18(projectDir, ID_PATH), "utf-8").trim();
|
|
5377
5565
|
return ID_SHAPE.test(id) ? id : null;
|
|
5378
5566
|
} catch {
|
|
5379
5567
|
return null;
|
|
@@ -5382,35 +5570,35 @@ function readId(projectDir) {
|
|
|
5382
5570
|
var sameFolder = (a, b) => a.ino === b.ino && a.birthtimeMs === b.birthtimeMs;
|
|
5383
5571
|
function isRecordedFolder(historyDir, folder) {
|
|
5384
5572
|
try {
|
|
5385
|
-
const was = JSON.parse(readFileSync15(
|
|
5573
|
+
const was = JSON.parse(readFileSync15(join18(historyDir, "project.json"), "utf-8"));
|
|
5386
5574
|
return sameFolder({ ino: was.ino, birthtimeMs: was.born }, folder);
|
|
5387
5575
|
} catch {
|
|
5388
5576
|
return false;
|
|
5389
5577
|
}
|
|
5390
5578
|
}
|
|
5391
5579
|
function projectHistoryId(projectDir, historyRoot) {
|
|
5392
|
-
const dir =
|
|
5580
|
+
const dir = resolve7(projectDir);
|
|
5393
5581
|
const folder = statSync8(dir);
|
|
5394
5582
|
let id = readId(dir);
|
|
5395
|
-
if (!id ||
|
|
5583
|
+
if (!id || existsSync10(join18(historyRoot, id)) && !isRecordedFolder(join18(historyRoot, id), folder)) {
|
|
5396
5584
|
id = randomUUID6();
|
|
5397
|
-
mkdirSync8(
|
|
5398
|
-
writeFileSync7(
|
|
5585
|
+
mkdirSync8(join18(dir, ".hyperframes"), { recursive: true });
|
|
5586
|
+
writeFileSync7(join18(dir, ID_PATH), `${id}
|
|
5399
5587
|
`);
|
|
5400
5588
|
}
|
|
5401
|
-
recordProject(
|
|
5589
|
+
recordProject(join18(historyRoot, id), dir, folder);
|
|
5402
5590
|
return id;
|
|
5403
5591
|
}
|
|
5404
5592
|
function recordProject(historyDir, dir, folder) {
|
|
5405
5593
|
const record = { dir, ino: folder.ino, born: folder.birthtimeMs };
|
|
5406
5594
|
mkdirSync8(historyDir, { recursive: true });
|
|
5407
|
-
replaceFileAtomically(
|
|
5595
|
+
replaceFileAtomically(join18(historyDir, "project.json"), JSON.stringify(record), 420);
|
|
5408
5596
|
}
|
|
5409
5597
|
|
|
5410
5598
|
// src/history/ownerLock.ts
|
|
5411
5599
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
5412
5600
|
import { linkSync, mkdirSync as mkdirSync9, readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
5413
|
-
import { join as
|
|
5601
|
+
import { join as join19 } from "path";
|
|
5414
5602
|
var HistoryBusyError = class extends Error {
|
|
5415
5603
|
constructor(pid) {
|
|
5416
5604
|
super(`This project's history is open in another process (pid ${pid}).`);
|
|
@@ -5469,7 +5657,7 @@ function evictDeadOwner(file) {
|
|
|
5469
5657
|
}
|
|
5470
5658
|
}
|
|
5471
5659
|
async function takeHistoryOwnership(home, waitMs) {
|
|
5472
|
-
const file =
|
|
5660
|
+
const file = join19(home, "owner.pid");
|
|
5473
5661
|
const deadline = Date.now() + waitMs;
|
|
5474
5662
|
mkdirSync9(home, { recursive: true });
|
|
5475
5663
|
for (; ; ) {
|
|
@@ -5490,7 +5678,7 @@ async function takeHistoryOwnership(home, waitMs) {
|
|
|
5490
5678
|
|
|
5491
5679
|
// src/history/historyLog.ts
|
|
5492
5680
|
import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync10, openSync as openSync5, readFileSync as readFileSync17, writeSync as writeSync2 } from "fs";
|
|
5493
|
-
import { dirname as
|
|
5681
|
+
import { dirname as dirname9 } from "path";
|
|
5494
5682
|
var START = "start";
|
|
5495
5683
|
function parseRecord(line) {
|
|
5496
5684
|
try {
|
|
@@ -5542,7 +5730,7 @@ function writeLog(file, log) {
|
|
|
5542
5730
|
...log.entries.map((entry) => ({ type: "entry", entry })),
|
|
5543
5731
|
...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
|
|
5544
5732
|
];
|
|
5545
|
-
mkdirSync10(
|
|
5733
|
+
mkdirSync10(dirname9(file), { recursive: true });
|
|
5546
5734
|
replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
|
|
5547
5735
|
`).join(""), 420);
|
|
5548
5736
|
}
|
|
@@ -5615,7 +5803,7 @@ function referencedHashes(log, current) {
|
|
|
5615
5803
|
}
|
|
5616
5804
|
|
|
5617
5805
|
// src/history/projectHistory.ts
|
|
5618
|
-
var DEFAULT_HISTORY_ROOT =
|
|
5806
|
+
var DEFAULT_HISTORY_ROOT = join20(homedir2(), ".cache", "hyperframes", "history");
|
|
5619
5807
|
var MAX_WINDOW_IDLE_MS = 10 * 6e4;
|
|
5620
5808
|
var UNDO_MODES = ["just-this", "back-to-before", "keep-later-edits"];
|
|
5621
5809
|
var OUTSIDE = { kind: "outside", name: "Outside" };
|
|
@@ -5641,20 +5829,20 @@ async function removeEmptyFolders(dir) {
|
|
|
5641
5829
|
throw error;
|
|
5642
5830
|
}
|
|
5643
5831
|
for (const entry of entries)
|
|
5644
|
-
if (entry.isDirectory()) await removeEmptyFolders(
|
|
5832
|
+
if (entry.isDirectory()) await removeEmptyFolders(join20(dir, entry.name));
|
|
5645
5833
|
await rmdir(dir);
|
|
5646
5834
|
}
|
|
5647
5835
|
function fileLeftIn(dir, at, deleted) {
|
|
5648
5836
|
for (const entry of readdirSync7(dir, { withFileTypes: true })) {
|
|
5649
5837
|
const path = `${at}/${entry.name}`;
|
|
5650
|
-
const left = entry.isDirectory() ? fileLeftIn(
|
|
5838
|
+
const left = entry.isDirectory() ? fileLeftIn(join20(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
|
|
5651
5839
|
if (left) return left;
|
|
5652
5840
|
}
|
|
5653
5841
|
return void 0;
|
|
5654
5842
|
}
|
|
5655
5843
|
function ignoresCase(dir) {
|
|
5656
|
-
const self = statSync9(
|
|
5657
|
-
const other = statSync9(
|
|
5844
|
+
const self = statSync9(join20(dir, ID_PATH), { throwIfNoEntry: false });
|
|
5845
|
+
const other = statSync9(join20(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
|
|
5658
5846
|
return self !== void 0 && other?.ino === self.ino && other.dev === self.dev;
|
|
5659
5847
|
}
|
|
5660
5848
|
var blocks = (removed, added) => added.startsWith(`${removed}/`) || removed.startsWith(`${added}/`);
|
|
@@ -5670,8 +5858,8 @@ var Engine = class {
|
|
|
5670
5858
|
this.options = options;
|
|
5671
5859
|
this.projectId = projectId;
|
|
5672
5860
|
this.blobs = blobs;
|
|
5673
|
-
this.dir =
|
|
5674
|
-
this.home =
|
|
5861
|
+
this.dir = resolve8(options.projectDir);
|
|
5862
|
+
this.home = join20(options.historyRoot, projectId);
|
|
5675
5863
|
this.foldsCase = ignoresCase(this.dir);
|
|
5676
5864
|
const folder = statSync9(this.dir, { throwIfNoEntry: false });
|
|
5677
5865
|
if (!folder || readId(this.dir) !== projectId || !isRecordedFolder(this.home, folder))
|
|
@@ -5704,7 +5892,7 @@ var Engine = class {
|
|
|
5704
5892
|
return this.foldsCase ? path.toLowerCase() : path;
|
|
5705
5893
|
}
|
|
5706
5894
|
get logFile() {
|
|
5707
|
-
return
|
|
5895
|
+
return join20(this.home, "log.jsonl");
|
|
5708
5896
|
}
|
|
5709
5897
|
now() {
|
|
5710
5898
|
return (this.options.now ?? Date.now)();
|
|
@@ -5732,7 +5920,7 @@ var Engine = class {
|
|
|
5732
5920
|
persistLog(record) {
|
|
5733
5921
|
if (record) saveRecord(this.logFile, this.log, record);
|
|
5734
5922
|
else writeLog(this.logFile, this.log);
|
|
5735
|
-
if (!
|
|
5923
|
+
if (!existsSync11(join20(this.home, "project.json")))
|
|
5736
5924
|
recordProject(this.home, this.dir, this.folder);
|
|
5737
5925
|
}
|
|
5738
5926
|
replaced() {
|
|
@@ -5782,7 +5970,7 @@ var Engine = class {
|
|
|
5782
5970
|
}
|
|
5783
5971
|
readStatCache() {
|
|
5784
5972
|
try {
|
|
5785
|
-
const files = JSON.parse(readFileSync18(
|
|
5973
|
+
const files = JSON.parse(readFileSync18(join20(this.home, "stat.json"), "utf-8"));
|
|
5786
5974
|
return new Map(Object.entries(files));
|
|
5787
5975
|
} catch {
|
|
5788
5976
|
return /* @__PURE__ */ new Map();
|
|
@@ -5791,7 +5979,7 @@ var Engine = class {
|
|
|
5791
5979
|
saveStatCache() {
|
|
5792
5980
|
const files = JSON.stringify(Object.fromEntries(this.tracked));
|
|
5793
5981
|
mkdirSync11(this.home, { recursive: true });
|
|
5794
|
-
replaceFileAtomically(
|
|
5982
|
+
replaceFileAtomically(join20(this.home, "stat.json"), files, 420);
|
|
5795
5983
|
}
|
|
5796
5984
|
/**
|
|
5797
5985
|
* Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
|
|
@@ -5831,23 +6019,23 @@ var Engine = class {
|
|
|
5831
6019
|
/** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
|
|
5832
6020
|
async storeIfPresent(path) {
|
|
5833
6021
|
try {
|
|
5834
|
-
return await this.blobs.put(
|
|
6022
|
+
return await this.blobs.put(join20(this.dir, path));
|
|
5835
6023
|
} catch (error) {
|
|
5836
6024
|
if (error.code === "ENOENT") return null;
|
|
5837
6025
|
throw error;
|
|
5838
6026
|
}
|
|
5839
6027
|
}
|
|
5840
|
-
async observe(path,
|
|
6028
|
+
async observe(path, stat3, at) {
|
|
5841
6029
|
const known = this.tracked.get(path) ?? { hash: null, stat: null };
|
|
5842
|
-
if (
|
|
6030
|
+
if (stat3 && known.stat === stat3) return false;
|
|
5843
6031
|
const hash = await this.storeIfPresent(path);
|
|
5844
6032
|
if (hash === null || this.whereFolder() !== "here") return false;
|
|
5845
|
-
this.tracked.set(path, { hash, stat:
|
|
6033
|
+
this.tracked.set(path, { hash, stat: stat3 });
|
|
5846
6034
|
if (known.hash !== hash) await this.record(path, known.hash, hash, at);
|
|
5847
|
-
return known.hash !== hash || known.stat !==
|
|
6035
|
+
return known.hash !== hash || known.stat !== stat3;
|
|
5848
6036
|
}
|
|
5849
6037
|
async record(path, before, after, at) {
|
|
5850
|
-
const endedBeforeThisWrite = this.windows.filter((
|
|
6038
|
+
const endedBeforeThisWrite = this.windows.filter((open2) => !takesWrite(open2, at));
|
|
5851
6039
|
if (endedBeforeThisWrite.length) await this.commitOutside();
|
|
5852
6040
|
for (const window2 of endedBeforeThisWrite) await this.endWindow(window2);
|
|
5853
6041
|
const window = this.windows.at(-1);
|
|
@@ -5880,7 +6068,7 @@ var Engine = class {
|
|
|
5880
6068
|
if (window.changes.size) await this.commitCut(window, [...window.changes.keys()]);
|
|
5881
6069
|
}
|
|
5882
6070
|
pendingElsewhere() {
|
|
5883
|
-
return Boolean(this.outside?.changes.size) || this.windows.some((
|
|
6071
|
+
return Boolean(this.outside?.changes.size) || this.windows.some((open2) => open2.changes.size > 0);
|
|
5884
6072
|
}
|
|
5885
6073
|
/** The held claim `taken` continues: same key, each file from its own last change, no held file changed since. */
|
|
5886
6074
|
heldFor(key, taken) {
|
|
@@ -5894,7 +6082,7 @@ var Engine = class {
|
|
|
5894
6082
|
return group && taken.every((change) => after(change.path, change.before) === change.before) && [...group.changes.values()].every(untouched) ? group : null;
|
|
5895
6083
|
}
|
|
5896
6084
|
logPath(path) {
|
|
5897
|
-
return
|
|
6085
|
+
return relative6(this.dir, resolve8(this.dir, path)).split(sep4).join("/");
|
|
5898
6086
|
}
|
|
5899
6087
|
/** Takes `paths`' uncommitted changes from others, cut at what `who` overwrote. */
|
|
5900
6088
|
async takeClaimed(who, paths, overwrote) {
|
|
@@ -5905,7 +6093,7 @@ var Engine = class {
|
|
|
5905
6093
|
hashOfVersion(version)
|
|
5906
6094
|
])
|
|
5907
6095
|
);
|
|
5908
|
-
const others = this.windows.filter((
|
|
6096
|
+
const others = this.windows.filter((open2) => !sameWho(open2.who, who));
|
|
5909
6097
|
const groups = this.outside ? [this.outside, ...others] : others;
|
|
5910
6098
|
const hits = groups.flatMap(
|
|
5911
6099
|
(group) => [...group.changes.values()].filter((change) => wanted.has(change.path)).map((change) => ({ group, change }))
|
|
@@ -5918,7 +6106,7 @@ var Engine = class {
|
|
|
5918
6106
|
}
|
|
5919
6107
|
hits.forEach(
|
|
5920
6108
|
({ change }, i) => forgetOverwrittenBytes(
|
|
5921
|
-
|
|
6109
|
+
join20(this.dir, change.path),
|
|
5922
6110
|
new Set([...walked[i]].map(hashVersion))
|
|
5923
6111
|
)
|
|
5924
6112
|
);
|
|
@@ -5926,7 +6114,7 @@ var Engine = class {
|
|
|
5926
6114
|
}
|
|
5927
6115
|
/** Walks the server's writes back from `change.after` to the bytes they replaced, stored so an undo restores them. */
|
|
5928
6116
|
async overwrittenBy(change, told, seen) {
|
|
5929
|
-
const absPath =
|
|
6117
|
+
const absPath = join20(this.dir, change.path);
|
|
5930
6118
|
let bytes;
|
|
5931
6119
|
for (let hash = change.after; hash; ) {
|
|
5932
6120
|
if (seen.has(hash)) return void 0;
|
|
@@ -5938,7 +6126,7 @@ var Engine = class {
|
|
|
5938
6126
|
if (hash === change.before || hash === told) return hash;
|
|
5939
6127
|
}
|
|
5940
6128
|
if (bytes === void 0) return void 0;
|
|
5941
|
-
const staged =
|
|
6129
|
+
const staged = join20(this.home, `overwrote-${randomUUID8()}`);
|
|
5942
6130
|
await writeFile(staged, bytes);
|
|
5943
6131
|
try {
|
|
5944
6132
|
return await this.blobs.put(staged);
|
|
@@ -6051,7 +6239,7 @@ var Engine = class {
|
|
|
6051
6239
|
}
|
|
6052
6240
|
/** A watcher saw a write: one sweep per burst takes it in (a deleted folder is reported by its name alone). */
|
|
6053
6241
|
noteChange(path) {
|
|
6054
|
-
if (this.notedTimer || !affectsProjectSignature(this.dir,
|
|
6242
|
+
if (this.notedTimer || !affectsProjectSignature(this.dir, resolve8(this.dir, path))) return;
|
|
6055
6243
|
this.notedTimer = setTimeout(() => {
|
|
6056
6244
|
this.notedTimer = null;
|
|
6057
6245
|
this.background(() => this.sweep());
|
|
@@ -6093,7 +6281,7 @@ var Engine = class {
|
|
|
6093
6281
|
if (!this.windows.includes(window)) return window.entry ?? null;
|
|
6094
6282
|
clearTimeout(window.idleTimer);
|
|
6095
6283
|
await this.commitClaim();
|
|
6096
|
-
this.windows = this.windows.filter((
|
|
6284
|
+
this.windows = this.windows.filter((open2) => open2 !== window);
|
|
6097
6285
|
window.entry = await this.commit(window) ?? window.entry ?? null;
|
|
6098
6286
|
return window.entry;
|
|
6099
6287
|
}
|
|
@@ -6109,9 +6297,9 @@ var Engine = class {
|
|
|
6109
6297
|
const parts = path.split("/");
|
|
6110
6298
|
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
6111
6299
|
const at = parts.slice(0, depth).join("/");
|
|
6112
|
-
const
|
|
6113
|
-
if (!
|
|
6114
|
-
if (at === path || !
|
|
6300
|
+
const stat3 = lstatSync4(join20(this.dir, at), { throwIfNoEntry: false });
|
|
6301
|
+
if (!stat3) return void 0;
|
|
6302
|
+
if (at === path || !stat3.isDirectory()) return { at, stat: stat3 };
|
|
6115
6303
|
}
|
|
6116
6304
|
return void 0;
|
|
6117
6305
|
}
|
|
@@ -6121,8 +6309,8 @@ var Engine = class {
|
|
|
6121
6309
|
if (!standing) return void 0;
|
|
6122
6310
|
if (standing.stat.isSymbolicLink()) return standing.at;
|
|
6123
6311
|
if (standing.at === path && !standing.stat.isDirectory()) return void 0;
|
|
6124
|
-
const real = realpathSync3.native(
|
|
6125
|
-
const at =
|
|
6312
|
+
const real = realpathSync3.native(join20(this.dir, standing.at));
|
|
6313
|
+
const at = relative6(realpathSync3.native(this.dir), real).split(sep4).join("/");
|
|
6126
6314
|
if (standing.at !== path) return deleted.has(at) ? void 0 : at;
|
|
6127
6315
|
return fileLeftIn(real, at, deleted);
|
|
6128
6316
|
}
|
|
@@ -6139,14 +6327,14 @@ var Engine = class {
|
|
|
6139
6327
|
for (const [path, hash] of changes) await this.writeProjectFile(path, hash);
|
|
6140
6328
|
await this.sweep();
|
|
6141
6329
|
} finally {
|
|
6142
|
-
this.windows = this.windows.filter((
|
|
6330
|
+
this.windows = this.windows.filter((open2) => open2 !== group);
|
|
6143
6331
|
}
|
|
6144
6332
|
return this.commit(group, extra);
|
|
6145
6333
|
}
|
|
6146
6334
|
/** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
|
|
6147
6335
|
async writeProjectFile(path, hash) {
|
|
6148
6336
|
this.assertWritable();
|
|
6149
|
-
const absPath =
|
|
6337
|
+
const absPath = join20(this.dir, path);
|
|
6150
6338
|
const { writeToken } = this;
|
|
6151
6339
|
if (writeToken) {
|
|
6152
6340
|
const version = hash === null ? DELETED_VERSION : hashVersion(hash);
|
|
@@ -6270,18 +6458,18 @@ var Engine = class {
|
|
|
6270
6458
|
},
|
|
6271
6459
|
checkout: (entryId, side, emptyDir) => this.queue(async () => {
|
|
6272
6460
|
this.entry(entryId);
|
|
6273
|
-
if (!
|
|
6274
|
-
const dest =
|
|
6461
|
+
if (!existsSync11(this.dir)) throw new Error(`The project folder is gone: ${this.dir}`);
|
|
6462
|
+
const dest = resolve8(emptyDir);
|
|
6275
6463
|
if (isWithin(this.dir, dest))
|
|
6276
6464
|
throw new Error(`Checkout writes outside the project only: ${dest}`);
|
|
6277
6465
|
if ((await readdir2(dest).catch(missingIsEmpty)).length > 0)
|
|
6278
6466
|
throw new Error(`Checkout writes into an empty folder only: ${dest}`);
|
|
6279
6467
|
const files = manifestAround(this.log, entryId, side);
|
|
6280
6468
|
try {
|
|
6281
|
-
for (const [path, hash] of files) await this.blobs.writeTo(hash,
|
|
6469
|
+
for (const [path, hash] of files) await this.blobs.writeTo(hash, join20(dest, path));
|
|
6282
6470
|
} catch (error) {
|
|
6283
6471
|
for (const name of await readdir2(dest).catch(() => []))
|
|
6284
|
-
await rm2(
|
|
6472
|
+
await rm2(join20(dest, name), { recursive: true, force: true });
|
|
6285
6473
|
throw error;
|
|
6286
6474
|
}
|
|
6287
6475
|
}),
|
|
@@ -6316,9 +6504,9 @@ var Engine = class {
|
|
|
6316
6504
|
}
|
|
6317
6505
|
};
|
|
6318
6506
|
function isWithin(dir, path) {
|
|
6319
|
-
let probe =
|
|
6320
|
-
while (!
|
|
6321
|
-
const rel =
|
|
6507
|
+
let probe = resolve8(path);
|
|
6508
|
+
while (!existsSync11(probe) && dirname10(probe) !== probe) probe = dirname10(probe);
|
|
6509
|
+
const rel = relative6(realpathSync3.native(dir), realpathSync3.native(probe));
|
|
6322
6510
|
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute3(rel);
|
|
6323
6511
|
}
|
|
6324
6512
|
function missingIsEmpty(error) {
|
|
@@ -6332,15 +6520,15 @@ var HistoryClosedError = class extends Error {
|
|
|
6332
6520
|
}
|
|
6333
6521
|
};
|
|
6334
6522
|
async function openProjectHistory(options) {
|
|
6335
|
-
if (!
|
|
6523
|
+
if (!existsSync11(options.projectDir))
|
|
6336
6524
|
throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
|
|
6337
6525
|
const projectId = projectHistoryId(options.projectDir, options.historyRoot);
|
|
6338
6526
|
const release = await takeHistoryOwnership(
|
|
6339
|
-
|
|
6527
|
+
join20(options.historyRoot, projectId),
|
|
6340
6528
|
options.ownerWaitMs ?? 5e3
|
|
6341
6529
|
);
|
|
6342
6530
|
try {
|
|
6343
|
-
const blobs = await openBlobStore(
|
|
6531
|
+
const blobs = await openBlobStore(join20(options.historyRoot, projectId, "blobs"));
|
|
6344
6532
|
const engine = new Engine(options, projectId, blobs);
|
|
6345
6533
|
await engine.queue(() => engine.start());
|
|
6346
6534
|
const api = engine.api();
|
|
@@ -6499,6 +6687,7 @@ function createStudioApi(adapter) {
|
|
|
6499
6687
|
registerLintRoutes(api, adapter);
|
|
6500
6688
|
registerRenderRoutes(api, adapter);
|
|
6501
6689
|
registerThumbnailRoutes(api, adapter);
|
|
6690
|
+
registerImageThumbnailRoutes(api, adapter);
|
|
6502
6691
|
registerSelectionRoutes(api, adapter);
|
|
6503
6692
|
registerMediaRoutes(api, adapter);
|
|
6504
6693
|
registerWaveformRoutes(api, adapter);
|
|
@@ -6510,14 +6699,14 @@ function createStudioApi(adapter) {
|
|
|
6510
6699
|
}
|
|
6511
6700
|
|
|
6512
6701
|
// src/history/historyCache.ts
|
|
6513
|
-
function historyCache(
|
|
6702
|
+
function historyCache(open2) {
|
|
6514
6703
|
const histories = /* @__PURE__ */ new Map();
|
|
6515
6704
|
const get = async (projectDir) => {
|
|
6516
6705
|
let cached = histories.get(projectDir);
|
|
6517
|
-
if (!cached) histories.set(projectDir, cached =
|
|
6706
|
+
if (!cached) histories.set(projectDir, cached = open2(projectDir));
|
|
6518
6707
|
const opened = await cached;
|
|
6519
6708
|
if (!opened?.replacedAtPath()) return opened;
|
|
6520
|
-
const reopen = () =>
|
|
6709
|
+
const reopen = () => open2(projectDir);
|
|
6521
6710
|
if (histories.get(projectDir) === cached)
|
|
6522
6711
|
histories.set(projectDir, opened.close().then(reopen, reopen));
|
|
6523
6712
|
return get(projectDir);
|
|
@@ -6534,6 +6723,7 @@ function historyCache(open) {
|
|
|
6534
6723
|
}
|
|
6535
6724
|
|
|
6536
6725
|
// src/helpers/duplicateElement.ts
|
|
6726
|
+
import { sameInstant } from "@hyperframes/core/clip-facts";
|
|
6537
6727
|
import { ensureHfIds as ensureHfIds4 } from "@hyperframes/parsers/hf-ids";
|
|
6538
6728
|
function duplicateElementInHtml(source, target, newId, at) {
|
|
6539
6729
|
const { document, wrappedFragment } = parseSourceDocument(source);
|
|
@@ -6568,9 +6758,10 @@ function rippleElements(document, element, at, duration, track) {
|
|
|
6568
6758
|
continue;
|
|
6569
6759
|
}
|
|
6570
6760
|
const start = numericAttribute(candidate, "data-start");
|
|
6571
|
-
if (start
|
|
6572
|
-
|
|
6573
|
-
|
|
6761
|
+
if (start === null) continue;
|
|
6762
|
+
const metInsertionPoint = sameInstant(start, at);
|
|
6763
|
+
if (start < at && !metInsertionPoint) continue;
|
|
6764
|
+
candidate.setAttribute("data-start", String((metInsertionPoint ? at : start) + duration));
|
|
6574
6765
|
}
|
|
6575
6766
|
}
|
|
6576
6767
|
function stripStableIds(element) {
|
|
@@ -6585,11 +6776,11 @@ function numericAttribute(element, name) {
|
|
|
6585
6776
|
}
|
|
6586
6777
|
|
|
6587
6778
|
// src/helpers/previewDocumentStore.ts
|
|
6588
|
-
import { createHash as
|
|
6779
|
+
import { createHash as createHash8 } from "crypto";
|
|
6589
6780
|
import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync8, rmSync as rmSync7 } from "fs";
|
|
6590
|
-
import { join as
|
|
6781
|
+
import { join as join21 } from "path";
|
|
6591
6782
|
function createPreviewDocumentStore(dir, salt) {
|
|
6592
|
-
const fileFor = (key) =>
|
|
6783
|
+
const fileFor = (key) => join21(dir, `${createHash8("sha256").update(`${salt}
|
|
6593
6784
|
${key}`).digest("hex").slice(0, 32)}.html`);
|
|
6594
6785
|
return {
|
|
6595
6786
|
read(key) {
|
|
@@ -6605,7 +6796,7 @@ ${key}`).digest("hex").slice(0, 32)}.html`);
|
|
|
6605
6796
|
const file = fileFor(key);
|
|
6606
6797
|
replaceFileAtomically(file, html, 420);
|
|
6607
6798
|
for (const name of readdirSync8(dir)) {
|
|
6608
|
-
if (
|
|
6799
|
+
if (join21(dir, name) !== file) rmSync7(join21(dir, name), { force: true });
|
|
6609
6800
|
}
|
|
6610
6801
|
} catch {
|
|
6611
6802
|
}
|
|
@@ -6692,6 +6883,7 @@ export {
|
|
|
6692
6883
|
STUDIO_MANUAL_EDITS_PATH,
|
|
6693
6884
|
STUDIO_MOTION_PATH,
|
|
6694
6885
|
UNDO_MODES,
|
|
6886
|
+
affectsPreview,
|
|
6695
6887
|
affectsProjectSignature,
|
|
6696
6888
|
applyFileMutations,
|
|
6697
6889
|
buildSubCompositionHtml,
|