@hyperframes/studio-server 0.7.61 → 0.7.63
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-LVXVG4V6.js → chunk-7Q5AFHU6.js} +3 -6
- package/dist/chunk-7Q5AFHU6.js.map +1 -0
- package/dist/{chunk-YBR7MXIO.js → chunk-IVOJZ24X.js} +9 -11
- package/dist/chunk-IVOJZ24X.js.map +1 -0
- package/dist/{chunk-ZUW4PULZ.js → chunk-OK7FKBKI.js} +3 -3
- package/dist/{chunk-ZUW4PULZ.js.map → chunk-OK7FKBKI.js.map} +1 -1
- package/dist/{chunk-XVQX2JHE.js → chunk-VPA335OG.js} +19 -4
- package/dist/chunk-VPA335OG.js.map +1 -0
- package/dist/helpers/mediaCodecMap.d.ts +3 -3
- 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.d.ts +3 -0
- package/dist/helpers/sourceMutation.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +557 -101
- package/dist/index.js.map +1 -1
- package/dist/{mediaProxyPreview-CeshDUjE.d.ts → mediaProxyPreview-ChE1ATG4.d.ts} +1 -1
- package/package.json +3 -3
- package/dist/chunk-LVXVG4V6.js.map +0 -1
- package/dist/chunk-XVQX2JHE.js.map +0 -1
- package/dist/chunk-YBR7MXIO.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
splitElementInHtml,
|
|
11
11
|
unwrapElementsFromHtml,
|
|
12
12
|
wrapElementsInHtml
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-VPA335OG.js";
|
|
14
14
|
import {
|
|
15
15
|
getElementScreenshotClip
|
|
16
16
|
} from "./chunk-W2SBTCO2.js";
|
|
@@ -19,12 +19,12 @@ import {
|
|
|
19
19
|
isAutoProxyEnabled,
|
|
20
20
|
proxyEtagSalt,
|
|
21
21
|
resolvePreviewMediaCodecProbeCache
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-OK7FKBKI.js";
|
|
23
23
|
import {
|
|
24
24
|
ProxyCapacityError,
|
|
25
25
|
ProxyTranscodeError,
|
|
26
26
|
resolveProxy
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-IVOJZ24X.js";
|
|
28
28
|
import {
|
|
29
29
|
PROXY_VARIANT_CONFIG,
|
|
30
30
|
decideMediaProxyEligibility,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
probeAssetCodec,
|
|
33
33
|
probeMediaMetadata,
|
|
34
34
|
resolveProxyVariantRequest
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-7Q5AFHU6.js";
|
|
36
36
|
import {
|
|
37
37
|
STUDIO_MANUAL_EDITS_PATH,
|
|
38
38
|
createStudioManualEditsRenderBodyScript,
|
|
@@ -334,10 +334,10 @@ function registerStoryboardRoutes(api, adapter) {
|
|
|
334
334
|
import { bodyLimit } from "hono/body-limit";
|
|
335
335
|
import {
|
|
336
336
|
closeSync,
|
|
337
|
-
existsSync as
|
|
337
|
+
existsSync as existsSync4,
|
|
338
338
|
ftruncateSync,
|
|
339
339
|
openSync,
|
|
340
|
-
readFileSync as
|
|
340
|
+
readFileSync as readFileSync5,
|
|
341
341
|
writeFileSync as writeFileSync4,
|
|
342
342
|
writeSync,
|
|
343
343
|
mkdirSync as mkdirSync3,
|
|
@@ -347,7 +347,7 @@ import {
|
|
|
347
347
|
renameSync,
|
|
348
348
|
readdirSync as readdirSync4
|
|
349
349
|
} from "fs";
|
|
350
|
-
import { resolve as
|
|
350
|
+
import { resolve as resolve3, dirname as dirname2, join as join6 } from "path";
|
|
351
351
|
|
|
352
352
|
// src/helpers/mime.ts
|
|
353
353
|
var MIME_TYPES = {
|
|
@@ -654,7 +654,196 @@ import {
|
|
|
654
654
|
scalePositionsInScript,
|
|
655
655
|
dedupePositionWritesInScript
|
|
656
656
|
} from "@hyperframes/parsers/gsap-writer-acorn";
|
|
657
|
+
import { parseHTML as parseHTML2 } from "linkedom";
|
|
658
|
+
|
|
659
|
+
// src/helpers/compositionInsertion.ts
|
|
660
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync } from "fs";
|
|
661
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
662
|
+
import { dirname, relative as relative3, resolve as resolve2, sep } from "path";
|
|
657
663
|
import { parseHTML } from "linkedom";
|
|
664
|
+
var CompositionInsertionError = class extends Error {
|
|
665
|
+
constructor(message, status) {
|
|
666
|
+
super(message);
|
|
667
|
+
this.status = status;
|
|
668
|
+
}
|
|
669
|
+
status;
|
|
670
|
+
};
|
|
671
|
+
function descendants(root, selector) {
|
|
672
|
+
const found = Array.from(root.querySelectorAll(selector));
|
|
673
|
+
for (const template of root.querySelectorAll("template")) {
|
|
674
|
+
found.push(...descendants(template, selector));
|
|
675
|
+
}
|
|
676
|
+
return [...new Set(found)];
|
|
677
|
+
}
|
|
678
|
+
function compositionRoot(source) {
|
|
679
|
+
const document = parseHTML(source).document;
|
|
680
|
+
const root = descendants(document, "[data-composition-id]")[0];
|
|
681
|
+
if (!root) throw new CompositionInsertionError("Composition source has no root", 400);
|
|
682
|
+
return { document, root };
|
|
683
|
+
}
|
|
684
|
+
function positiveAttribute(root, ...names) {
|
|
685
|
+
for (const name of names) {
|
|
686
|
+
const value = Number.parseFloat(root.getAttribute(name) ?? "");
|
|
687
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
688
|
+
}
|
|
689
|
+
throw new CompositionInsertionError(`Composition source has no valid ${names[0]}`, 400);
|
|
690
|
+
}
|
|
691
|
+
function canonicalProjectPath(projectDir, candidate) {
|
|
692
|
+
if (!candidate) {
|
|
693
|
+
throw new CompositionInsertionError("Composition source escapes the project", 400);
|
|
694
|
+
}
|
|
695
|
+
if (!existsSync3(candidate)) {
|
|
696
|
+
throw new CompositionInsertionError("Composition source was not found", 404);
|
|
697
|
+
}
|
|
698
|
+
const canonical = realpathSync(candidate);
|
|
699
|
+
if (!isSafePath(realpathSync(projectDir), canonical)) {
|
|
700
|
+
throw new CompositionInsertionError("Composition source escapes the project", 400);
|
|
701
|
+
}
|
|
702
|
+
return canonical;
|
|
703
|
+
}
|
|
704
|
+
function validateSourcePath(sourcePath) {
|
|
705
|
+
if (!sourcePath.trim() || sourcePath.includes("\0") || /^[a-z]+:/i.test(sourcePath)) {
|
|
706
|
+
throw new CompositionInsertionError("Invalid composition source path", 400);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function canonicalProjectFile(projectDir, sourcePath) {
|
|
710
|
+
validateSourcePath(sourcePath);
|
|
711
|
+
return canonicalProjectPath(projectDir, resolveWithinProject(projectDir, sourcePath));
|
|
712
|
+
}
|
|
713
|
+
function canonicalDependency(projectDir, ownerAbs, sourcePath) {
|
|
714
|
+
validateSourcePath(sourcePath);
|
|
715
|
+
return canonicalProjectPath(
|
|
716
|
+
projectDir,
|
|
717
|
+
resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname(ownerAbs), sourcePath)))
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
|
|
721
|
+
const visited = /* @__PURE__ */ new Set();
|
|
722
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
723
|
+
const visit = (file) => {
|
|
724
|
+
if (file === targetAbs) {
|
|
725
|
+
throw new CompositionInsertionError("Composition insertion would create a cycle", 400);
|
|
726
|
+
}
|
|
727
|
+
if (visiting.has(file)) {
|
|
728
|
+
throw new CompositionInsertionError("Composition dependency cycle detected", 400);
|
|
729
|
+
}
|
|
730
|
+
if (visited.has(file)) return;
|
|
731
|
+
visiting.add(file);
|
|
732
|
+
const source = readFileSync4(file, "utf-8");
|
|
733
|
+
const { document } = compositionRoot(source);
|
|
734
|
+
for (const host of descendants(document, "[data-composition-src]")) {
|
|
735
|
+
const dependency = host.getAttribute("data-composition-src");
|
|
736
|
+
if (dependency) {
|
|
737
|
+
visit(canonicalDependency(projectDir, file, dependency));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
visiting.delete(file);
|
|
741
|
+
visited.add(file);
|
|
742
|
+
};
|
|
743
|
+
visit(sourceAbs);
|
|
744
|
+
}
|
|
745
|
+
function numberAttribute(element, name, fallback = 0) {
|
|
746
|
+
const value = Number.parseFloat(element.getAttribute(name) ?? "");
|
|
747
|
+
return Number.isFinite(value) ? value : fallback;
|
|
748
|
+
}
|
|
749
|
+
function rangesOverlap(start, duration, other) {
|
|
750
|
+
const otherStart = numberAttribute(other, "data-start");
|
|
751
|
+
const otherDuration = numberAttribute(other, "data-duration");
|
|
752
|
+
return start < otherStart + otherDuration && otherStart < start + duration;
|
|
753
|
+
}
|
|
754
|
+
function resolveTrack(root, desiredTrack, start, duration) {
|
|
755
|
+
const clips = descendants(root, "[data-start][data-duration]").filter(
|
|
756
|
+
(element) => element !== root && element.parentElement?.closest("[data-composition-id]") === root
|
|
757
|
+
);
|
|
758
|
+
const tracks = [...new Set(clips.map((clip) => numberAttribute(clip, "data-track-index")))].sort(
|
|
759
|
+
(a, b) => a - b
|
|
760
|
+
);
|
|
761
|
+
const isFree = (track) => !clips.some(
|
|
762
|
+
(clip) => numberAttribute(clip, "data-track-index") === track && rangesOverlap(start, duration, clip)
|
|
763
|
+
);
|
|
764
|
+
if (isFree(desiredTrack)) return desiredTrack;
|
|
765
|
+
const row = tracks.indexOf(desiredTrack);
|
|
766
|
+
for (let index = row - 1; index >= 0; index--) {
|
|
767
|
+
const track = tracks[index];
|
|
768
|
+
if (track !== void 0 && isFree(track)) return track;
|
|
769
|
+
}
|
|
770
|
+
for (let index = Math.max(0, row + 1); index < tracks.length; index++) {
|
|
771
|
+
const track = tracks[index];
|
|
772
|
+
if (track !== void 0 && isFree(track)) return track;
|
|
773
|
+
}
|
|
774
|
+
return Math.max(desiredTrack, ...tracks, -1) + 1;
|
|
775
|
+
}
|
|
776
|
+
function uniqueHostId(root, base) {
|
|
777
|
+
const ids = /* @__PURE__ */ new Set([
|
|
778
|
+
...descendants(root, "[id]").map((element) => element.id),
|
|
779
|
+
...descendants(root, "[data-composition-id]").flatMap((element) => {
|
|
780
|
+
const id = element.getAttribute("data-composition-id");
|
|
781
|
+
return id ? [id] : [];
|
|
782
|
+
})
|
|
783
|
+
]);
|
|
784
|
+
if (!ids.has(base)) return base;
|
|
785
|
+
let suffix = 2;
|
|
786
|
+
while (ids.has(`${base}_${suffix}`)) suffix += 1;
|
|
787
|
+
return `${base}_${suffix}`;
|
|
788
|
+
}
|
|
789
|
+
function relativeSourcePath(targetAbs, sourceAbs) {
|
|
790
|
+
return relative3(dirname(targetAbs), sourceAbs).split(sep).join("/");
|
|
791
|
+
}
|
|
792
|
+
function insertCompositionIntoSource(input) {
|
|
793
|
+
const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
|
|
794
|
+
const sourceAbs = canonicalProjectFile(input.projectDir, input.sourcePath);
|
|
795
|
+
validateDependencyGraph(input.projectDir, targetAbs, sourceAbs);
|
|
796
|
+
const source = readFileSync4(sourceAbs, "utf-8");
|
|
797
|
+
const sourceComposition = compositionRoot(source).root;
|
|
798
|
+
const duration = positiveAttribute(
|
|
799
|
+
sourceComposition,
|
|
800
|
+
"data-composition-duration",
|
|
801
|
+
"data-duration"
|
|
802
|
+
);
|
|
803
|
+
const width = positiveAttribute(sourceComposition, "data-width");
|
|
804
|
+
const height = positiveAttribute(sourceComposition, "data-height");
|
|
805
|
+
const { document, root } = compositionRoot(input.parentSource);
|
|
806
|
+
const parentDuration = positiveAttribute(root, "data-duration", "data-composition-duration");
|
|
807
|
+
const base = (sourceComposition.getAttribute("data-composition-id") ?? "composition").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "composition";
|
|
808
|
+
const hostId = uniqueHostId(root, base);
|
|
809
|
+
const track = resolveTrack(
|
|
810
|
+
root,
|
|
811
|
+
Math.max(0, Math.round(input.desiredTrack)),
|
|
812
|
+
input.start,
|
|
813
|
+
duration
|
|
814
|
+
);
|
|
815
|
+
const zIndex = Math.max(
|
|
816
|
+
0,
|
|
817
|
+
...descendants(root, "[style]").map((element) => {
|
|
818
|
+
const match = /(?:^|;)\s*z-index\s*:\s*(-?\d+)/i.exec(element.getAttribute("style") ?? "");
|
|
819
|
+
return match?.[1] ? Number.parseInt(match[1], 10) : 0;
|
|
820
|
+
})
|
|
821
|
+
) + 1;
|
|
822
|
+
const host = document.createElement("div");
|
|
823
|
+
host.id = hostId;
|
|
824
|
+
host.className = "clip";
|
|
825
|
+
host.setAttribute("data-hf-id", `hf-${randomUUID2()}`);
|
|
826
|
+
host.setAttribute("data-composition-id", hostId);
|
|
827
|
+
host.setAttribute("data-composition-src", relativeSourcePath(targetAbs, sourceAbs));
|
|
828
|
+
host.setAttribute("data-start", String(Math.round(input.start * 100) / 100));
|
|
829
|
+
host.setAttribute("data-duration", String(duration));
|
|
830
|
+
host.setAttribute("data-playback-start", "0");
|
|
831
|
+
host.setAttribute("data-track-index", String(track));
|
|
832
|
+
host.setAttribute("data-width", String(width));
|
|
833
|
+
host.setAttribute("data-height", String(height));
|
|
834
|
+
host.setAttribute(
|
|
835
|
+
"style",
|
|
836
|
+
`position: absolute; left: 0px; top: 0px; width: ${width}px; height: ${height}px; z-index: ${zIndex}`
|
|
837
|
+
);
|
|
838
|
+
root.appendChild(host);
|
|
839
|
+
if (input.start + duration > parentDuration) {
|
|
840
|
+
const name = root.hasAttribute("data-duration") ? "data-duration" : "data-composition-duration";
|
|
841
|
+
root.setAttribute(name, String(Math.round((input.start + duration) * 100) / 100));
|
|
842
|
+
}
|
|
843
|
+
return { html: document.toString(), hostId, track, duration };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/routes/files.ts
|
|
658
847
|
function isAcornGsapWriterEnabled() {
|
|
659
848
|
const val = process.env["STUDIO_SDK_CUTOVER_ENABLED"];
|
|
660
849
|
return val === "true" || val === "1";
|
|
@@ -676,7 +865,7 @@ async function resolveProjectPath(c, adapter, pathPrefix, opts) {
|
|
|
676
865
|
if (!absPath) {
|
|
677
866
|
return { error: c.json({ error: "forbidden" }, 403) };
|
|
678
867
|
}
|
|
679
|
-
if (opts?.mustExist && !
|
|
868
|
+
if (opts?.mustExist && !existsSync4(absPath)) {
|
|
680
869
|
return { error: c.json({ error: "not found" }, 404) };
|
|
681
870
|
}
|
|
682
871
|
return { project, filePath, absPath };
|
|
@@ -687,6 +876,25 @@ function resolveProjectFile(c, adapter, opts) {
|
|
|
687
876
|
function resolveFileMutationContext(c, adapter, operation) {
|
|
688
877
|
return resolveProjectPath(c, adapter, (id) => `/projects/${id}/file-mutations/${operation}/`);
|
|
689
878
|
}
|
|
879
|
+
function isAtomicCutTarget(value) {
|
|
880
|
+
if (!value || typeof value !== "object") return false;
|
|
881
|
+
const target = value;
|
|
882
|
+
return !!target.target && typeof target.target === "object" && Number.isFinite(target.splitTime) && Number.isFinite(target.elementStart) && Number.isFinite(target.elementDuration) && Number(target.elementDuration) > 0;
|
|
883
|
+
}
|
|
884
|
+
function isAtomicCutFileRequest(value) {
|
|
885
|
+
if (!value || typeof value !== "object") return false;
|
|
886
|
+
const file = value;
|
|
887
|
+
return typeof file.path === "string" && file.path.length > 0 && typeof file.expectedVersion === "string" && Array.isArray(file.targets) && file.targets.length > 0 && file.targets.every(isAtomicCutTarget);
|
|
888
|
+
}
|
|
889
|
+
var atomicCutTail = Promise.resolve();
|
|
890
|
+
function serializeAtomicCut(task) {
|
|
891
|
+
const next = atomicCutTail.then(task, task);
|
|
892
|
+
atomicCutTail = next.then(
|
|
893
|
+
() => void 0,
|
|
894
|
+
() => void 0
|
|
895
|
+
);
|
|
896
|
+
return next;
|
|
897
|
+
}
|
|
690
898
|
function isElementPatchRequest(value) {
|
|
691
899
|
if (typeof value !== "object" || value === null) return false;
|
|
692
900
|
if (!("target" in value) || typeof value.target !== "object" || value.target === null) {
|
|
@@ -722,7 +930,7 @@ function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSyn
|
|
|
722
930
|
resolvedPaths.add(absPath);
|
|
723
931
|
let before;
|
|
724
932
|
try {
|
|
725
|
-
before =
|
|
933
|
+
before = readFileSync5(absPath, "utf-8");
|
|
726
934
|
} catch {
|
|
727
935
|
return { error: "not-found", sourceFile: batch.sourceFile };
|
|
728
936
|
}
|
|
@@ -834,8 +1042,8 @@ async function parseMutationBody(c) {
|
|
|
834
1042
|
return { target: body.target, body };
|
|
835
1043
|
}
|
|
836
1044
|
function ensureDir(filePath) {
|
|
837
|
-
const dir =
|
|
838
|
-
if (!
|
|
1045
|
+
const dir = dirname2(filePath);
|
|
1046
|
+
if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
|
|
839
1047
|
}
|
|
840
1048
|
function generateCopyPath(projectDir, originalPath) {
|
|
841
1049
|
const ext = originalPath.includes(".") ? "." + originalPath.split(".").pop() : "";
|
|
@@ -844,7 +1052,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
844
1052
|
const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
|
|
845
1053
|
let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
|
|
846
1054
|
let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
|
|
847
|
-
while (
|
|
1055
|
+
while (existsSync4(resolve3(projectDir, candidate))) {
|
|
848
1056
|
num++;
|
|
849
1057
|
candidate = `${cleanBase} (copy ${num})${ext}`;
|
|
850
1058
|
}
|
|
@@ -871,7 +1079,7 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
871
1079
|
);
|
|
872
1080
|
let updatedCount = 0;
|
|
873
1081
|
for (const file of textFiles) {
|
|
874
|
-
const content =
|
|
1082
|
+
const content = readFileSync5(file, "utf-8");
|
|
875
1083
|
if (!content.includes(oldPath)) continue;
|
|
876
1084
|
const updated = content.split(oldPath).join(newPath);
|
|
877
1085
|
if (updated !== content) {
|
|
@@ -882,7 +1090,7 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
882
1090
|
return updatedCount;
|
|
883
1091
|
}
|
|
884
1092
|
function extractGsapScriptBlock(html) {
|
|
885
|
-
const { document } =
|
|
1093
|
+
const { document } = parseHTML2(html);
|
|
886
1094
|
const scripts = [
|
|
887
1095
|
...document.querySelectorAll("script:not([src])"),
|
|
888
1096
|
...Array.from(document.querySelectorAll("template")).flatMap(
|
|
@@ -1128,7 +1336,7 @@ function validateGsapMutationRequest(c, body) {
|
|
|
1128
1336
|
return null;
|
|
1129
1337
|
}
|
|
1130
1338
|
async function prepareGsapMutationScript(c, res, firstMutation) {
|
|
1131
|
-
const beforeHtml =
|
|
1339
|
+
const beforeHtml = readFileSync5(res.absPath, "utf-8");
|
|
1132
1340
|
let html = beforeHtml;
|
|
1133
1341
|
let block = extractGsapScriptBlock(html);
|
|
1134
1342
|
if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
|
|
@@ -1188,7 +1396,7 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1188
1396
|
const changed = block.scriptText !== initialScript;
|
|
1189
1397
|
const newHtml = changed ? block.replaceScript(block.scriptText) : html;
|
|
1190
1398
|
let backupPath = null;
|
|
1191
|
-
if (
|
|
1399
|
+
if (readFileSync5(res.absPath, "utf-8") !== beforeHtml) {
|
|
1192
1400
|
return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
|
|
1193
1401
|
}
|
|
1194
1402
|
if (changed) {
|
|
@@ -1811,6 +2019,74 @@ async function executeGsapMutationRecast(body, block, respond) {
|
|
|
1811
2019
|
return respond({ error: `unknown mutation type: ${body.type}` }, 400);
|
|
1812
2020
|
}
|
|
1813
2021
|
}
|
|
2022
|
+
async function foldAtomicCutFile(c, file, absPath, before) {
|
|
2023
|
+
let after = before;
|
|
2024
|
+
let splitCount = 0;
|
|
2025
|
+
const skippedSelectors = /* @__PURE__ */ new Set();
|
|
2026
|
+
const respond = (data, status) => status ? c.json(data, status) : c.json(data);
|
|
2027
|
+
const orderedTargets = file.targets.map((cut, index) => ({ cut, index })).sort((left, right) => {
|
|
2028
|
+
const locatorKey = (entry) => !entry.target.id && !entry.target.hfId && entry.target.selector ? entry.target.selector : null;
|
|
2029
|
+
const leftKey = locatorKey(left.cut);
|
|
2030
|
+
const rightKey = locatorKey(right.cut);
|
|
2031
|
+
if (leftKey && rightKey) {
|
|
2032
|
+
return leftKey.localeCompare(rightKey) || (right.cut.target.selectorIndex ?? 0) - (left.cut.target.selectorIndex ?? 0);
|
|
2033
|
+
}
|
|
2034
|
+
if (leftKey) return -1;
|
|
2035
|
+
if (rightKey) return 1;
|
|
2036
|
+
return left.index - right.index;
|
|
2037
|
+
}).map(({ cut }) => cut);
|
|
2038
|
+
for (const cut of orderedTargets) {
|
|
2039
|
+
const baseId = cut.originalId || cut.target.id || "clip";
|
|
2040
|
+
const split = splitElementInHtml(after, cut.target, cut.splitTime, `${baseId}-split`, {
|
|
2041
|
+
start: cut.elementStart,
|
|
2042
|
+
duration: cut.elementDuration,
|
|
2043
|
+
playbackStart: cut.playbackStart,
|
|
2044
|
+
playbackRate: cut.playbackRate,
|
|
2045
|
+
stampPlaybackStart: cut.isComposition
|
|
2046
|
+
});
|
|
2047
|
+
if (!split.matched || !split.newId) {
|
|
2048
|
+
return c.json(
|
|
2049
|
+
{ error: `Cut target was not found or was outside its authored bounds in ${file.path}` },
|
|
2050
|
+
400
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
after = split.html;
|
|
2054
|
+
splitCount++;
|
|
2055
|
+
if (!cut.originalId) continue;
|
|
2056
|
+
const block = extractGsapScriptBlock(after);
|
|
2057
|
+
if (!block) continue;
|
|
2058
|
+
const result = await executeGsapMutation(
|
|
2059
|
+
{
|
|
2060
|
+
type: "split-animations",
|
|
2061
|
+
originalId: cut.originalId,
|
|
2062
|
+
newId: split.newId,
|
|
2063
|
+
splitTime: cut.splitTime,
|
|
2064
|
+
elementStart: cut.elementStart,
|
|
2065
|
+
elementDuration: cut.elementDuration
|
|
2066
|
+
},
|
|
2067
|
+
block,
|
|
2068
|
+
respond
|
|
2069
|
+
);
|
|
2070
|
+
if (result instanceof Response) return result;
|
|
2071
|
+
let script = typeof result === "string" ? result : result.script;
|
|
2072
|
+
if (typeof result !== "string") {
|
|
2073
|
+
for (const selector of result.skippedSelectors) skippedSelectors.add(selector);
|
|
2074
|
+
}
|
|
2075
|
+
if (script !== block.scriptText) {
|
|
2076
|
+
const parser = await loadGsapParser();
|
|
2077
|
+
script = parser.syncPositionHoldsBeforeKeyframes(script);
|
|
2078
|
+
after = block.replaceScript(script);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
return {
|
|
2082
|
+
path: file.path,
|
|
2083
|
+
absPath,
|
|
2084
|
+
before,
|
|
2085
|
+
after,
|
|
2086
|
+
splitCount,
|
|
2087
|
+
skippedSelectors: [...skippedSelectors]
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
1814
2090
|
async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
1815
2091
|
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
1816
2092
|
const uploaded = [];
|
|
@@ -1826,23 +2102,23 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
1826
2102
|
skipped.push(name);
|
|
1827
2103
|
continue;
|
|
1828
2104
|
}
|
|
1829
|
-
const destPath =
|
|
2105
|
+
const destPath = resolve3(targetDir, name);
|
|
1830
2106
|
if (!isSafePath(projectDir, destPath)) continue;
|
|
1831
2107
|
let finalPath = destPath;
|
|
1832
2108
|
let finalName = name;
|
|
1833
|
-
if (
|
|
2109
|
+
if (existsSync4(finalPath)) {
|
|
1834
2110
|
const dotIdx = name.indexOf(".", name.startsWith(".") ? 1 : 0);
|
|
1835
2111
|
const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
|
|
1836
2112
|
const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
|
1837
2113
|
let n = 2;
|
|
1838
2114
|
const MAX_COPY_INDEX = 1e4;
|
|
1839
|
-
while (n < MAX_COPY_INDEX &&
|
|
2115
|
+
while (n < MAX_COPY_INDEX && existsSync4(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
|
|
1840
2116
|
if (n >= MAX_COPY_INDEX) {
|
|
1841
2117
|
skipped.push(name);
|
|
1842
2118
|
continue;
|
|
1843
2119
|
}
|
|
1844
2120
|
finalName = `${base} (${n})${ext}`;
|
|
1845
|
-
finalPath =
|
|
2121
|
+
finalPath = resolve3(targetDir, finalName);
|
|
1846
2122
|
}
|
|
1847
2123
|
const buffer = Buffer.from(await value.arrayBuffer());
|
|
1848
2124
|
const validation = validateUploadedMediaBuffer(finalName, buffer);
|
|
@@ -1864,13 +2140,13 @@ function registerFileRoutes(api, adapter) {
|
|
|
1864
2140
|
api.get("/projects/:id/files/*", async (c) => {
|
|
1865
2141
|
const res = await resolveProjectFile(c, adapter);
|
|
1866
2142
|
if ("error" in res) return res.error;
|
|
1867
|
-
if (!
|
|
2143
|
+
if (!existsSync4(res.absPath)) {
|
|
1868
2144
|
if (c.req.query("optional") === "1") {
|
|
1869
2145
|
return c.json({ filename: res.filePath, content: "" });
|
|
1870
2146
|
}
|
|
1871
2147
|
return c.json({ error: "not found" }, 404);
|
|
1872
2148
|
}
|
|
1873
|
-
const content =
|
|
2149
|
+
const content = readFileSync5(res.absPath, "utf-8");
|
|
1874
2150
|
const version = fileContentVersion(content);
|
|
1875
2151
|
c.header("ETag", version);
|
|
1876
2152
|
return c.json({ filename: res.filePath, content, version });
|
|
@@ -1884,7 +2160,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
1884
2160
|
if (expectedVersion === null && !createOnly) {
|
|
1885
2161
|
let currentContent = null;
|
|
1886
2162
|
try {
|
|
1887
|
-
currentContent =
|
|
2163
|
+
currentContent = readFileSync5(res.absPath, "utf-8");
|
|
1888
2164
|
} catch (error) {
|
|
1889
2165
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
1890
2166
|
throw error;
|
|
@@ -1910,7 +2186,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
1910
2186
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
1911
2187
|
throw error;
|
|
1912
2188
|
}
|
|
1913
|
-
const currentContent =
|
|
2189
|
+
const currentContent = readFileSync5(res.absPath, "utf-8");
|
|
1914
2190
|
return c.json(
|
|
1915
2191
|
{
|
|
1916
2192
|
error: "file conflict",
|
|
@@ -1945,7 +2221,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
1945
2221
|
);
|
|
1946
2222
|
}
|
|
1947
2223
|
try {
|
|
1948
|
-
const currentContent =
|
|
2224
|
+
const currentContent = readFileSync5(fd, "utf-8");
|
|
1949
2225
|
const currentVersion = fileContentVersion(currentContent);
|
|
1950
2226
|
if (expectedVersion !== currentVersion) {
|
|
1951
2227
|
return c.json(
|
|
@@ -1982,7 +2258,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
1982
2258
|
api.post("/projects/:id/files/*", async (c) => {
|
|
1983
2259
|
const res = await resolveProjectFile(c, adapter);
|
|
1984
2260
|
if ("error" in res) return res.error;
|
|
1985
|
-
if (
|
|
2261
|
+
if (existsSync4(res.absPath)) {
|
|
1986
2262
|
return c.json({ error: "already exists" }, 409);
|
|
1987
2263
|
}
|
|
1988
2264
|
ensureDir(res.absPath);
|
|
@@ -2006,15 +2282,71 @@ function registerFileRoutes(api, adapter) {
|
|
|
2006
2282
|
backupPath: backupPathForResponse(res.project.dir, backup.backupPath)
|
|
2007
2283
|
});
|
|
2008
2284
|
});
|
|
2285
|
+
api.post("/projects/:id/file-mutations/insert-composition/*", async (c) => {
|
|
2286
|
+
const ctx = await resolveFileMutationContext(c, adapter, "insert-composition");
|
|
2287
|
+
if ("error" in ctx) return ctx.error;
|
|
2288
|
+
const body = await c.req.json().catch(() => null);
|
|
2289
|
+
if (!body || typeof body.sourcePath !== "string" || typeof body.start !== "number" || !Number.isFinite(body.start) || body.start < 0 || typeof body.track !== "number" || !Number.isFinite(body.track) || typeof body.expectedVersion !== "string") {
|
|
2290
|
+
return c.json({ error: "sourcePath, finite placement, and expectedVersion required" }, 400);
|
|
2291
|
+
}
|
|
2292
|
+
let before;
|
|
2293
|
+
try {
|
|
2294
|
+
before = readFileSync5(ctx.absPath, "utf-8");
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2297
|
+
throw error;
|
|
2298
|
+
}
|
|
2299
|
+
return c.json({ error: "not found" }, 404);
|
|
2300
|
+
}
|
|
2301
|
+
const currentVersion = fileContentVersion(before);
|
|
2302
|
+
if (body.expectedVersion !== currentVersion) {
|
|
2303
|
+
return c.json({ error: "file conflict", currentVersion, currentContent: before }, 409);
|
|
2304
|
+
}
|
|
2305
|
+
let insertion;
|
|
2306
|
+
try {
|
|
2307
|
+
insertion = insertCompositionIntoSource({
|
|
2308
|
+
projectDir: ctx.project.dir,
|
|
2309
|
+
targetPath: ctx.filePath,
|
|
2310
|
+
sourcePath: body.sourcePath,
|
|
2311
|
+
parentSource: before,
|
|
2312
|
+
start: body.start,
|
|
2313
|
+
desiredTrack: body.track
|
|
2314
|
+
});
|
|
2315
|
+
} catch (error) {
|
|
2316
|
+
if (error instanceof CompositionInsertionError) {
|
|
2317
|
+
return c.json({ error: error.message }, error.status);
|
|
2318
|
+
}
|
|
2319
|
+
throw error;
|
|
2320
|
+
}
|
|
2321
|
+
const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
|
|
2322
|
+
if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
|
|
2323
|
+
writeFileSync4(ctx.absPath, insertion.html, "utf-8");
|
|
2324
|
+
const version = fileContentVersion(insertion.html);
|
|
2325
|
+
const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
|
|
2326
|
+
recordFileWriteReceipt(ctx.absPath, { path: ctx.filePath, version, writeToken });
|
|
2327
|
+
c.header("ETag", version);
|
|
2328
|
+
return c.json({
|
|
2329
|
+
ok: true,
|
|
2330
|
+
path: ctx.filePath,
|
|
2331
|
+
hostId: insertion.hostId,
|
|
2332
|
+
track: insertion.track,
|
|
2333
|
+
duration: insertion.duration,
|
|
2334
|
+
before,
|
|
2335
|
+
after: insertion.html,
|
|
2336
|
+
version,
|
|
2337
|
+
writeToken,
|
|
2338
|
+
backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
|
|
2339
|
+
});
|
|
2340
|
+
});
|
|
2009
2341
|
api.post("/projects/:id/file-mutations/remove-element/*", async (c) => {
|
|
2010
2342
|
const ctx = await resolveFileMutationContext(c, adapter, "remove-element");
|
|
2011
2343
|
if ("error" in ctx) return ctx.error;
|
|
2012
|
-
if (!
|
|
2344
|
+
if (!existsSync4(ctx.absPath)) {
|
|
2013
2345
|
return c.json({ error: "not found" }, 404);
|
|
2014
2346
|
}
|
|
2015
2347
|
const parsed = await parseMutationBody(c);
|
|
2016
2348
|
if ("error" in parsed) return parsed.error;
|
|
2017
|
-
const originalContent =
|
|
2349
|
+
const originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2018
2350
|
return writeIfChanged(
|
|
2019
2351
|
c,
|
|
2020
2352
|
ctx.project.dir,
|
|
@@ -2024,6 +2356,130 @@ function registerFileRoutes(api, adapter) {
|
|
|
2024
2356
|
removeElementFromHtml(originalContent, parsed.target)
|
|
2025
2357
|
);
|
|
2026
2358
|
});
|
|
2359
|
+
api.post("/projects/:id/file-mutations/split-batch", async (c) => {
|
|
2360
|
+
const body = await c.req.json().catch(() => null);
|
|
2361
|
+
if (!Array.isArray(body?.files) || body.files.length === 0 || !body.files.every(isAtomicCutFileRequest)) {
|
|
2362
|
+
return c.json({ error: "files with path, expectedVersion, and cut targets required" }, 400);
|
|
2363
|
+
}
|
|
2364
|
+
const files = body.files;
|
|
2365
|
+
const project = await adapter.resolveProject(c.req.param("id"));
|
|
2366
|
+
if (!project) return c.json({ error: "not found" }, 404);
|
|
2367
|
+
return serializeAtomicCut(async () => {
|
|
2368
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2369
|
+
const prepared = [];
|
|
2370
|
+
for (const file of files) {
|
|
2371
|
+
const absPath = resolveWithinProject(project.dir, file.path);
|
|
2372
|
+
if (!absPath) return c.json({ error: `forbidden path: ${file.path}` }, 403);
|
|
2373
|
+
if (seen.has(absPath)) return c.json({ error: `duplicate path: ${file.path}` }, 400);
|
|
2374
|
+
seen.add(absPath);
|
|
2375
|
+
let before;
|
|
2376
|
+
try {
|
|
2377
|
+
before = readFileSync5(absPath, "utf-8");
|
|
2378
|
+
} catch {
|
|
2379
|
+
return c.json({ error: `not found: ${file.path}` }, 404);
|
|
2380
|
+
}
|
|
2381
|
+
const currentVersion = fileContentVersion(before);
|
|
2382
|
+
if (currentVersion !== file.expectedVersion) {
|
|
2383
|
+
return c.json(
|
|
2384
|
+
{
|
|
2385
|
+
error: `file conflict: ${file.path}`,
|
|
2386
|
+
path: file.path,
|
|
2387
|
+
currentVersion,
|
|
2388
|
+
currentContent: before
|
|
2389
|
+
},
|
|
2390
|
+
409
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2393
|
+
let folded;
|
|
2394
|
+
try {
|
|
2395
|
+
folded = await foldAtomicCutFile(c, file, absPath, before);
|
|
2396
|
+
} catch (error) {
|
|
2397
|
+
const message = error instanceof Error ? error.message : "Cut transform failed";
|
|
2398
|
+
return c.json({ error: message }, 400);
|
|
2399
|
+
}
|
|
2400
|
+
if (folded instanceof Response) return folded;
|
|
2401
|
+
prepared.push(folded);
|
|
2402
|
+
}
|
|
2403
|
+
for (const file of prepared) {
|
|
2404
|
+
const current = readFileSync5(file.absPath, "utf-8");
|
|
2405
|
+
if (current !== file.before) {
|
|
2406
|
+
return c.json(
|
|
2407
|
+
{
|
|
2408
|
+
error: `file conflict: ${file.path}`,
|
|
2409
|
+
path: file.path,
|
|
2410
|
+
currentVersion: fileContentVersion(current),
|
|
2411
|
+
currentContent: current
|
|
2412
|
+
},
|
|
2413
|
+
409
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
const backups = /* @__PURE__ */ new Map();
|
|
2418
|
+
for (const file of prepared) {
|
|
2419
|
+
const backup = snapshotBeforeWrite(project.dir, file.absPath);
|
|
2420
|
+
if (backup.error) {
|
|
2421
|
+
return c.json(
|
|
2422
|
+
{ error: `Failed to create backup for ${file.path}: ${backup.error}` },
|
|
2423
|
+
500
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
backups.set(file.path, backupPathForResponse(project.dir, backup.backupPath));
|
|
2427
|
+
}
|
|
2428
|
+
const writeToken = createWriteToken(
|
|
2429
|
+
typeof body.transactionToken === "string" ? body.transactionToken : c.req.header("X-Hyperframes-Write-Token")
|
|
2430
|
+
);
|
|
2431
|
+
const written = [];
|
|
2432
|
+
try {
|
|
2433
|
+
for (const file of prepared) {
|
|
2434
|
+
writeFileSync4(file.absPath, file.after, "utf-8");
|
|
2435
|
+
written.push(file);
|
|
2436
|
+
recordFileWriteReceipt(file.absPath, {
|
|
2437
|
+
path: file.path,
|
|
2438
|
+
version: fileContentVersion(file.after),
|
|
2439
|
+
writeToken
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
} catch (error) {
|
|
2443
|
+
const conflicts = [];
|
|
2444
|
+
for (const file of written.reverse()) {
|
|
2445
|
+
try {
|
|
2446
|
+
const current = readFileSync5(file.absPath, "utf-8");
|
|
2447
|
+
if (current !== file.after) {
|
|
2448
|
+
conflicts.push(file.path);
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
writeFileSync4(file.absPath, file.before, "utf-8");
|
|
2452
|
+
recordFileWriteReceipt(file.absPath, {
|
|
2453
|
+
path: file.path,
|
|
2454
|
+
version: fileContentVersion(file.before),
|
|
2455
|
+
writeToken
|
|
2456
|
+
});
|
|
2457
|
+
} catch {
|
|
2458
|
+
conflicts.push(file.path);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
return c.json(
|
|
2462
|
+
{
|
|
2463
|
+
error: error instanceof Error ? error.message : "Cut write failed",
|
|
2464
|
+
outcome: conflicts.length ? "aborted-with-conflicts" : "aborted-restored",
|
|
2465
|
+
conflicts
|
|
2466
|
+
},
|
|
2467
|
+
conflicts.length ? 409 : 500
|
|
2468
|
+
);
|
|
2469
|
+
}
|
|
2470
|
+
const result = prepared.map((file) => ({
|
|
2471
|
+
path: file.path,
|
|
2472
|
+
before: file.before,
|
|
2473
|
+
after: file.after,
|
|
2474
|
+
version: fileContentVersion(file.after),
|
|
2475
|
+
writeToken,
|
|
2476
|
+
backupPath: backups.get(file.path) ?? null,
|
|
2477
|
+
splitCount: file.splitCount,
|
|
2478
|
+
skippedSelectors: file.skippedSelectors
|
|
2479
|
+
}));
|
|
2480
|
+
return c.json({ ok: true, outcome: "committed", files: result });
|
|
2481
|
+
});
|
|
2482
|
+
});
|
|
2027
2483
|
api.post("/projects/:id/file-mutations/split-element/*", async (c) => {
|
|
2028
2484
|
const ctx = await resolveFileMutationContext(c, adapter, "split-element");
|
|
2029
2485
|
if ("error" in ctx) return ctx.error;
|
|
@@ -2035,7 +2491,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2035
2491
|
const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
|
|
2036
2492
|
let originalContent;
|
|
2037
2493
|
try {
|
|
2038
|
-
originalContent =
|
|
2494
|
+
originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2039
2495
|
} catch {
|
|
2040
2496
|
return c.json({ error: "not found" }, 404);
|
|
2041
2497
|
}
|
|
@@ -2086,7 +2542,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2086
2542
|
}
|
|
2087
2543
|
let originalContent;
|
|
2088
2544
|
try {
|
|
2089
|
-
originalContent =
|
|
2545
|
+
originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2090
2546
|
} catch {
|
|
2091
2547
|
return c.json({ error: "not found" }, 404);
|
|
2092
2548
|
}
|
|
@@ -2176,7 +2632,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2176
2632
|
}
|
|
2177
2633
|
let originalContent;
|
|
2178
2634
|
try {
|
|
2179
|
-
originalContent =
|
|
2635
|
+
originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2180
2636
|
} catch {
|
|
2181
2637
|
return c.json({ error: "not found" }, 404);
|
|
2182
2638
|
}
|
|
@@ -2218,7 +2674,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2218
2674
|
if ("error" in parsed) return parsed.error;
|
|
2219
2675
|
let originalContent;
|
|
2220
2676
|
try {
|
|
2221
|
-
originalContent =
|
|
2677
|
+
originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2222
2678
|
} catch {
|
|
2223
2679
|
return c.json({ error: "not found" }, 404);
|
|
2224
2680
|
}
|
|
@@ -2247,7 +2703,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2247
2703
|
if ("error" in parsed) return parsed.error;
|
|
2248
2704
|
let content;
|
|
2249
2705
|
try {
|
|
2250
|
-
content =
|
|
2706
|
+
content = readFileSync5(ctx.absPath, "utf-8");
|
|
2251
2707
|
} catch {
|
|
2252
2708
|
return c.json({ exists: false });
|
|
2253
2709
|
}
|
|
@@ -2265,7 +2721,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2265
2721
|
if (!newAbs) {
|
|
2266
2722
|
return c.json({ error: "forbidden" }, 403);
|
|
2267
2723
|
}
|
|
2268
|
-
if (
|
|
2724
|
+
if (existsSync4(newAbs)) {
|
|
2269
2725
|
return c.json({ error: "already exists" }, 409);
|
|
2270
2726
|
}
|
|
2271
2727
|
ensureDir(newAbs);
|
|
@@ -2281,7 +2737,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2281
2737
|
return c.json({ error: "path required" }, 400);
|
|
2282
2738
|
}
|
|
2283
2739
|
const srcAbs = resolveWithinProject(project.dir, body.path);
|
|
2284
|
-
if (!srcAbs || !
|
|
2740
|
+
if (!srcAbs || !existsSync4(srcAbs)) {
|
|
2285
2741
|
return c.json({ error: "not found" }, 404);
|
|
2286
2742
|
}
|
|
2287
2743
|
const copyPath = generateCopyPath(project.dir, body.path);
|
|
@@ -2290,7 +2746,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2290
2746
|
return c.json({ error: "forbidden" }, 403);
|
|
2291
2747
|
}
|
|
2292
2748
|
ensureDir(destAbs);
|
|
2293
|
-
writeFileSync4(destAbs,
|
|
2749
|
+
writeFileSync4(destAbs, readFileSync5(srcAbs));
|
|
2294
2750
|
return c.json({ ok: true, path: copyPath }, 201);
|
|
2295
2751
|
});
|
|
2296
2752
|
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
@@ -2306,7 +2762,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2306
2762
|
const subDir = c.req.query("dir") ?? "";
|
|
2307
2763
|
const targetDir = subDir ? resolveWithinProject(project.dir, subDir) : project.dir;
|
|
2308
2764
|
if (!targetDir) return c.json({ error: "forbidden" }, 403);
|
|
2309
|
-
if (subDir && !
|
|
2765
|
+
if (subDir && !existsSync4(targetDir)) mkdirSync3(targetDir, { recursive: true });
|
|
2310
2766
|
const formData = await c.req.formData();
|
|
2311
2767
|
const result = await processUploadedFiles(formData, targetDir, project.dir);
|
|
2312
2768
|
return c.json(
|
|
@@ -2320,7 +2776,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2320
2776
|
mustExist: true
|
|
2321
2777
|
});
|
|
2322
2778
|
if ("error" in res) return res.error;
|
|
2323
|
-
const html =
|
|
2779
|
+
const html = readFileSync5(res.absPath, "utf-8");
|
|
2324
2780
|
const block = extractGsapScriptBlock(html);
|
|
2325
2781
|
if (!block) {
|
|
2326
2782
|
return c.json({
|
|
@@ -2379,7 +2835,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2379
2835
|
if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
|
|
2380
2836
|
return c.json({ error: "expected and restore contents required" }, 400);
|
|
2381
2837
|
}
|
|
2382
|
-
const current =
|
|
2838
|
+
const current = readFileSync5(res.absPath, "utf-8");
|
|
2383
2839
|
if (current !== body.expected) {
|
|
2384
2840
|
return c.json({ ok: true, restored: false, conflict: true });
|
|
2385
2841
|
}
|
|
@@ -2389,15 +2845,15 @@ function registerFileRoutes(api, adapter) {
|
|
|
2389
2845
|
}
|
|
2390
2846
|
|
|
2391
2847
|
// src/routes/preview.ts
|
|
2392
|
-
import { existsSync as
|
|
2848
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
|
|
2393
2849
|
import { join as join8 } from "path";
|
|
2394
2850
|
import { createHash as createHash3 } from "crypto";
|
|
2395
2851
|
import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
|
|
2396
2852
|
|
|
2397
2853
|
// src/helpers/subComposition.ts
|
|
2398
|
-
import { existsSync as
|
|
2854
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
2399
2855
|
import { join as join7 } from "path";
|
|
2400
|
-
import { parseHTML as
|
|
2856
|
+
import { parseHTML as parseHTML3 } from "linkedom";
|
|
2401
2857
|
import {
|
|
2402
2858
|
rewriteAssetPaths,
|
|
2403
2859
|
rewriteCssAssetUrls,
|
|
@@ -2445,7 +2901,7 @@ function fixDigitLeadingIdSelectors(root) {
|
|
|
2445
2901
|
}
|
|
2446
2902
|
}
|
|
2447
2903
|
function extractFullDocumentParts(rawHtml, compPath) {
|
|
2448
|
-
const { document: doc } =
|
|
2904
|
+
const { document: doc } = parseHTML3(rawHtml);
|
|
2449
2905
|
const rewriteTargets = [doc.head, doc.body].filter(Boolean);
|
|
2450
2906
|
for (const target of rewriteTargets) {
|
|
2451
2907
|
rewriteRelativePaths(target, compPath);
|
|
@@ -2459,7 +2915,7 @@ function extractFullDocumentParts(rawHtml, compPath) {
|
|
|
2459
2915
|
return { headContent, bodyContent, htmlAttrs, bodyAttrs };
|
|
2460
2916
|
}
|
|
2461
2917
|
function extractTemplateInnerHtml(rawComp) {
|
|
2462
|
-
const { document: doc } =
|
|
2918
|
+
const { document: doc } = parseHTML3(rawComp);
|
|
2463
2919
|
const template = doc.querySelector("template");
|
|
2464
2920
|
return template ? template.innerHTML : null;
|
|
2465
2921
|
}
|
|
@@ -2503,15 +2959,15 @@ function tagRootCompositionFile(bodyHtml, compPath) {
|
|
|
2503
2959
|
}
|
|
2504
2960
|
function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
|
|
2505
2961
|
const compFile = join7(projectDir, compPath);
|
|
2506
|
-
if (!
|
|
2507
|
-
const rawComp = rawOverride ??
|
|
2962
|
+
if (!existsSync5(compFile)) return null;
|
|
2963
|
+
const rawComp = rawOverride ?? readFileSync6(compFile, "utf-8");
|
|
2508
2964
|
let compHeadContent = "";
|
|
2509
2965
|
let rewrittenContent;
|
|
2510
2966
|
let htmlAttrs = "";
|
|
2511
2967
|
let bodyAttrs = "";
|
|
2512
2968
|
const templateInner = extractTemplateInnerHtml(rawComp);
|
|
2513
2969
|
if (templateInner != null) {
|
|
2514
|
-
const { document: contentDoc } =
|
|
2970
|
+
const { document: contentDoc } = parseHTML3(
|
|
2515
2971
|
`<!DOCTYPE html><html><head></head><body>${templateInner}</body></html>`
|
|
2516
2972
|
);
|
|
2517
2973
|
rewriteRelativePaths(contentDoc, compPath);
|
|
@@ -2525,7 +2981,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
2525
2981
|
htmlAttrs = parts.htmlAttrs;
|
|
2526
2982
|
bodyAttrs = parts.bodyAttrs;
|
|
2527
2983
|
} else {
|
|
2528
|
-
const { document: contentDoc } =
|
|
2984
|
+
const { document: contentDoc } = parseHTML3(
|
|
2529
2985
|
`<!DOCTYPE html><html><head></head><body>${rawComp}</body></html>`
|
|
2530
2986
|
);
|
|
2531
2987
|
rewriteRelativePaths(contentDoc, compPath);
|
|
@@ -2536,8 +2992,8 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
2536
2992
|
rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
|
|
2537
2993
|
const indexPath = join7(projectDir, "index.html");
|
|
2538
2994
|
let headContent = "";
|
|
2539
|
-
if (
|
|
2540
|
-
const indexHtml =
|
|
2995
|
+
if (existsSync5(indexPath)) {
|
|
2996
|
+
const indexHtml = readFileSync6(indexPath, "utf-8");
|
|
2541
2997
|
const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
2542
2998
|
headContent = headMatch?.[1] ?? "";
|
|
2543
2999
|
}
|
|
@@ -2581,7 +3037,7 @@ import {
|
|
|
2581
3037
|
fstatSync,
|
|
2582
3038
|
ftruncateSync as ftruncateSync2,
|
|
2583
3039
|
openSync as openSync2,
|
|
2584
|
-
readFileSync as
|
|
3040
|
+
readFileSync as readFileSync7,
|
|
2585
3041
|
writeFileSync as writeFileSync5,
|
|
2586
3042
|
writeSync as writeSync2
|
|
2587
3043
|
} from "fs";
|
|
@@ -2591,7 +3047,7 @@ function persistHfIdsIfNeeded(filePath, html) {
|
|
|
2591
3047
|
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
2592
3048
|
if (idsAfter > idsBefore) {
|
|
2593
3049
|
try {
|
|
2594
|
-
const current =
|
|
3050
|
+
const current = readFileSync7(filePath, "utf-8");
|
|
2595
3051
|
if (current === html) {
|
|
2596
3052
|
writeFileSync5(filePath, normalized, "utf-8");
|
|
2597
3053
|
}
|
|
@@ -2619,7 +3075,7 @@ function stampFileHfIds(filePath) {
|
|
|
2619
3075
|
if (fd === null) return null;
|
|
2620
3076
|
try {
|
|
2621
3077
|
if (!fstatSync(fd).isFile()) return null;
|
|
2622
|
-
const html =
|
|
3078
|
+
const html = readFileSync7(fd, "utf-8");
|
|
2623
3079
|
const normalized = ensureHfIds(html);
|
|
2624
3080
|
const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
|
|
2625
3081
|
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
@@ -2663,9 +3119,9 @@ ${html}`;
|
|
|
2663
3119
|
}
|
|
2664
3120
|
function readStudioMotionManifestContent(projectDir) {
|
|
2665
3121
|
const manifestPath = join8(projectDir, STUDIO_MOTION_PATH);
|
|
2666
|
-
if (!
|
|
3122
|
+
if (!existsSync6(manifestPath)) return "";
|
|
2667
3123
|
try {
|
|
2668
|
-
return
|
|
3124
|
+
return readFileSync8(manifestPath, "utf-8");
|
|
2669
3125
|
} catch {
|
|
2670
3126
|
return "";
|
|
2671
3127
|
}
|
|
@@ -2821,16 +3277,16 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
|
|
|
2821
3277
|
}
|
|
2822
3278
|
function resolveProjectMainHtml(projectDir, projectId) {
|
|
2823
3279
|
const indexPath = join8(projectDir, "index.html");
|
|
2824
|
-
if (
|
|
3280
|
+
if (existsSync6(indexPath)) {
|
|
2825
3281
|
return {
|
|
2826
|
-
html:
|
|
3282
|
+
html: readFileSync8(indexPath, "utf-8"),
|
|
2827
3283
|
compositionPath: "index.html"
|
|
2828
3284
|
};
|
|
2829
3285
|
}
|
|
2830
3286
|
const blockHtmlPath = join8(projectDir, `${projectId}.html`);
|
|
2831
|
-
if (
|
|
3287
|
+
if (existsSync6(blockHtmlPath)) {
|
|
2832
3288
|
return {
|
|
2833
|
-
html:
|
|
3289
|
+
html: readFileSync8(blockHtmlPath, "utf-8"),
|
|
2834
3290
|
compositionPath: `${projectId}.html`
|
|
2835
3291
|
};
|
|
2836
3292
|
}
|
|
@@ -2935,7 +3391,7 @@ ${runtimeTag}`;
|
|
|
2935
3391
|
c.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? ""
|
|
2936
3392
|
);
|
|
2937
3393
|
const compFile = resolveWithinProject(project.dir, compPath);
|
|
2938
|
-
if (!compFile || !
|
|
3394
|
+
if (!compFile || !existsSync6(compFile) || !statSync2(compFile).isFile()) {
|
|
2939
3395
|
return c.text("not found", 404);
|
|
2940
3396
|
}
|
|
2941
3397
|
const etag = `"comp:v2:${compPath}:${signature}${variablesEtagSalt(vars.raw)}"`;
|
|
@@ -2973,7 +3429,7 @@ ${runtimeTag}`;
|
|
|
2973
3429
|
if (!file) {
|
|
2974
3430
|
return c.text("not found", 404);
|
|
2975
3431
|
}
|
|
2976
|
-
const stat =
|
|
3432
|
+
const stat = existsSync6(file) ? statSync2(file) : null;
|
|
2977
3433
|
if (!stat?.isFile()) {
|
|
2978
3434
|
return c.text("not found", 404);
|
|
2979
3435
|
}
|
|
@@ -3021,7 +3477,7 @@ ${runtimeTag}`;
|
|
|
3021
3477
|
}
|
|
3022
3478
|
servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
|
|
3023
3479
|
}
|
|
3024
|
-
const buffer = isText ? Buffer.from(
|
|
3480
|
+
const buffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : readFileSync8(servedPath);
|
|
3025
3481
|
const totalSize = buffer.length;
|
|
3026
3482
|
const rangeHeader = c.req.header("Range");
|
|
3027
3483
|
if (rangeHeader) {
|
|
@@ -3055,7 +3511,7 @@ ${runtimeTag}`;
|
|
|
3055
3511
|
}
|
|
3056
3512
|
|
|
3057
3513
|
// src/routes/lint.ts
|
|
3058
|
-
import { readFileSync as
|
|
3514
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
3059
3515
|
import { join as join9 } from "path";
|
|
3060
3516
|
function registerLintRoutes(api, adapter) {
|
|
3061
3517
|
api.get("/projects/:id/lint", async (c) => {
|
|
@@ -3067,7 +3523,7 @@ function registerLintRoutes(api, adapter) {
|
|
|
3067
3523
|
);
|
|
3068
3524
|
const allFindings = [];
|
|
3069
3525
|
for (const file of htmlFiles) {
|
|
3070
|
-
const content =
|
|
3526
|
+
const content = readFileSync9(join9(project.dir, file), "utf-8");
|
|
3071
3527
|
const result = await adapter.lint(content, { filePath: file });
|
|
3072
3528
|
if (result?.findings) {
|
|
3073
3529
|
for (const f of result.findings) {
|
|
@@ -3085,7 +3541,7 @@ function registerLintRoutes(api, adapter) {
|
|
|
3085
3541
|
|
|
3086
3542
|
// src/routes/render.ts
|
|
3087
3543
|
import { streamSSE } from "hono/streaming";
|
|
3088
|
-
import { existsSync as
|
|
3544
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
3089
3545
|
import { join as join10 } from "path";
|
|
3090
3546
|
import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
|
|
3091
3547
|
import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
|
|
@@ -3144,7 +3600,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3144
3600
|
const now = /* @__PURE__ */ new Date();
|
|
3145
3601
|
const jobId = `${project.id}_${formatRenderOutputTimestamp(now)}`;
|
|
3146
3602
|
const rendersDir = adapter.rendersDir(project);
|
|
3147
|
-
if (!
|
|
3603
|
+
if (!existsSync7(rendersDir)) mkdirSync4(rendersDir, { recursive: true });
|
|
3148
3604
|
const ext = FORMAT_EXT[format] ?? ".mp4";
|
|
3149
3605
|
const outputPath = join10(rendersDir, `${jobId}${ext}`);
|
|
3150
3606
|
const jobState = adapter.startRender({
|
|
@@ -3209,12 +3665,12 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3209
3665
|
api.get("/render/:jobId/view", (c) => {
|
|
3210
3666
|
const { jobId } = c.req.param();
|
|
3211
3667
|
const job = renderJobs.get(jobId);
|
|
3212
|
-
if (!job?.outputPath || !
|
|
3668
|
+
if (!job?.outputPath || !existsSync7(job.outputPath)) {
|
|
3213
3669
|
return c.json({ error: "not found" }, 404);
|
|
3214
3670
|
}
|
|
3215
3671
|
const contentType = renderContentType(job.outputPath);
|
|
3216
3672
|
const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
|
|
3217
|
-
const content =
|
|
3673
|
+
const content = readFileSync10(job.outputPath);
|
|
3218
3674
|
return new Response(content, {
|
|
3219
3675
|
headers: {
|
|
3220
3676
|
"Content-Type": contentType,
|
|
@@ -3227,12 +3683,12 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3227
3683
|
api.get("/render/:jobId/download", (c) => {
|
|
3228
3684
|
const { jobId } = c.req.param();
|
|
3229
3685
|
const job = renderJobs.get(jobId);
|
|
3230
|
-
if (!job?.outputPath || !
|
|
3686
|
+
if (!job?.outputPath || !existsSync7(job.outputPath)) {
|
|
3231
3687
|
return c.json({ error: "not found" }, 404);
|
|
3232
3688
|
}
|
|
3233
3689
|
const contentType = renderContentType(job.outputPath);
|
|
3234
3690
|
const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
|
|
3235
|
-
const content =
|
|
3691
|
+
const content = readFileSync10(job.outputPath);
|
|
3236
3692
|
return new Response(content, {
|
|
3237
3693
|
headers: {
|
|
3238
3694
|
"Content-Type": contentType,
|
|
@@ -3247,7 +3703,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3247
3703
|
const dir = state.outputPath.replace(/\/[^/]+$/, "");
|
|
3248
3704
|
for (const ext of [".mp4", ".webm", ".mov", ".meta.json"]) {
|
|
3249
3705
|
const fp = join10(dir, `${jobId}${ext}`);
|
|
3250
|
-
if (
|
|
3706
|
+
if (existsSync7(fp)) unlinkSync3(fp);
|
|
3251
3707
|
}
|
|
3252
3708
|
break;
|
|
3253
3709
|
}
|
|
@@ -3263,9 +3719,9 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3263
3719
|
const rendersDir = adapter.rendersDir(project);
|
|
3264
3720
|
const fp = resolveWithinProject(rendersDir, filename);
|
|
3265
3721
|
if (!fp) return c.json({ error: "forbidden" }, 403);
|
|
3266
|
-
if (!
|
|
3722
|
+
if (!existsSync7(fp)) return c.json({ error: "not found" }, 404);
|
|
3267
3723
|
const contentType = renderContentType(fp);
|
|
3268
|
-
const content =
|
|
3724
|
+
const content = readFileSync10(fp);
|
|
3269
3725
|
return new Response(content, {
|
|
3270
3726
|
headers: {
|
|
3271
3727
|
"Content-Type": contentType,
|
|
@@ -3279,7 +3735,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3279
3735
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
3280
3736
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
3281
3737
|
const rendersDir = adapter.rendersDir(project);
|
|
3282
|
-
if (!
|
|
3738
|
+
if (!existsSync7(rendersDir)) return c.json({ renders: [] });
|
|
3283
3739
|
const files = readdirSync5(rendersDir).filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov")).map((f) => {
|
|
3284
3740
|
const fp = join10(rendersDir, f);
|
|
3285
3741
|
const stat = statSync3(fp);
|
|
@@ -3287,10 +3743,10 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3287
3743
|
const metaPath = join10(rendersDir, `${rid}.meta.json`);
|
|
3288
3744
|
let status = "complete";
|
|
3289
3745
|
let durationMs;
|
|
3290
|
-
if (
|
|
3746
|
+
if (existsSync7(metaPath)) {
|
|
3291
3747
|
try {
|
|
3292
|
-
const meta = JSON.parse(
|
|
3293
|
-
if (meta.status === "failed") status = "failed";
|
|
3748
|
+
const meta = JSON.parse(readFileSync10(metaPath, "utf-8"));
|
|
3749
|
+
if (meta.status === "failed" && !existsSync7(fp)) status = "failed";
|
|
3294
3750
|
if (meta.durationMs) durationMs = meta.durationMs;
|
|
3295
3751
|
} catch {
|
|
3296
3752
|
}
|
|
@@ -3320,7 +3776,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3320
3776
|
}
|
|
3321
3777
|
|
|
3322
3778
|
// src/routes/thumbnail.ts
|
|
3323
|
-
import { existsSync as
|
|
3779
|
+
import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
|
|
3324
3780
|
import { join as join11 } from "path";
|
|
3325
3781
|
import { createHash as createHash4 } from "crypto";
|
|
3326
3782
|
var THUMBNAIL_CACHE_VERSION = "v4";
|
|
@@ -3352,8 +3808,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
3352
3808
|
let sourceMtime = 0;
|
|
3353
3809
|
let sourceKey = "";
|
|
3354
3810
|
const htmlFile = join11(project.dir, compPath);
|
|
3355
|
-
if (
|
|
3356
|
-
const html =
|
|
3811
|
+
if (existsSync8(htmlFile)) {
|
|
3812
|
+
const html = readFileSync11(htmlFile, "utf-8");
|
|
3357
3813
|
sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
|
|
3358
3814
|
sourceMtime = Math.round(statSync4(htmlFile).mtimeMs);
|
|
3359
3815
|
if (!vpWidth) {
|
|
@@ -3365,15 +3821,15 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
3365
3821
|
}
|
|
3366
3822
|
const manualEditsFile = join11(project.dir, STUDIO_MANUAL_EDITS_PATH);
|
|
3367
3823
|
let manualEditsKey = "";
|
|
3368
|
-
if (
|
|
3369
|
-
const manualEditsContent =
|
|
3824
|
+
if (existsSync8(manualEditsFile)) {
|
|
3825
|
+
const manualEditsContent = readFileSync11(manualEditsFile, "utf-8");
|
|
3370
3826
|
manualEditsKey = `_${createHash4("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
|
|
3371
3827
|
sourceMtime = Math.max(sourceMtime, Math.round(statSync4(manualEditsFile).mtimeMs));
|
|
3372
3828
|
}
|
|
3373
3829
|
const motionFile = join11(project.dir, STUDIO_MOTION_PATH);
|
|
3374
3830
|
let motionKey = "";
|
|
3375
|
-
if (
|
|
3376
|
-
const motionContent =
|
|
3831
|
+
if (existsSync8(motionFile)) {
|
|
3832
|
+
const motionContent = readFileSync11(motionFile, "utf-8");
|
|
3377
3833
|
motionKey = `_${createHash4("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
|
|
3378
3834
|
sourceMtime = Math.max(sourceMtime, Math.round(statSync4(motionFile).mtimeMs));
|
|
3379
3835
|
}
|
|
@@ -3383,8 +3839,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
3383
3839
|
const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
|
|
3384
3840
|
const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${manualEditsKey}${motionKey}${sourceKey}_${format}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
|
|
3385
3841
|
const cachePath = join11(cacheDir, cacheKey);
|
|
3386
|
-
if (
|
|
3387
|
-
return new Response(new Uint8Array(
|
|
3842
|
+
if (existsSync8(cachePath)) {
|
|
3843
|
+
return new Response(new Uint8Array(readFileSync11(cachePath)), {
|
|
3388
3844
|
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
3389
3845
|
});
|
|
3390
3846
|
}
|
|
@@ -3406,7 +3862,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
3406
3862
|
500
|
|
3407
3863
|
);
|
|
3408
3864
|
}
|
|
3409
|
-
if (!
|
|
3865
|
+
if (!existsSync8(cacheDir)) mkdirSync5(cacheDir, { recursive: true });
|
|
3410
3866
|
writeFileSync6(cachePath, buffer);
|
|
3411
3867
|
return new Response(new Uint8Array(buffer), {
|
|
3412
3868
|
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
@@ -3419,7 +3875,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
3419
3875
|
}
|
|
3420
3876
|
|
|
3421
3877
|
// src/routes/waveform.ts
|
|
3422
|
-
import { existsSync as
|
|
3878
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
|
|
3423
3879
|
import { join as join12 } from "path";
|
|
3424
3880
|
function registerWaveformRoutes(api, adapter) {
|
|
3425
3881
|
api.get("/projects/:id/waveform/*", async (c) => {
|
|
@@ -3429,12 +3885,12 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
3429
3885
|
c.req.path.replace(`/projects/${project.id}/waveform/`, "").split("?")[0] ?? ""
|
|
3430
3886
|
);
|
|
3431
3887
|
const audioPath = join12(project.dir, assetPath);
|
|
3432
|
-
if (!
|
|
3888
|
+
if (!existsSync9(audioPath)) return c.json({ error: "file not found" }, 404);
|
|
3433
3889
|
const cacheDir = join12(project.dir, ".waveform-cache");
|
|
3434
3890
|
const cachePath = join12(cacheDir, buildWaveformCacheKey(assetPath));
|
|
3435
|
-
if (
|
|
3891
|
+
if (existsSync9(cachePath)) {
|
|
3436
3892
|
try {
|
|
3437
|
-
const peaks2 = JSON.parse(
|
|
3893
|
+
const peaks2 = JSON.parse(readFileSync12(cachePath, "utf-8"));
|
|
3438
3894
|
return c.json({ peaks: peaks2 });
|
|
3439
3895
|
} catch {
|
|
3440
3896
|
}
|
|
@@ -3718,8 +4174,8 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
3718
4174
|
|
|
3719
4175
|
// src/routes/media.ts
|
|
3720
4176
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
3721
|
-
import { existsSync as
|
|
3722
|
-
import { basename as basename2, dirname as
|
|
4177
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
|
|
4178
|
+
import { basename as basename2, dirname as dirname3, extname as extname2, join as join13 } from "path";
|
|
3723
4179
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3724
4180
|
".mp4",
|
|
3725
4181
|
".mov",
|
|
@@ -3756,7 +4212,7 @@ function uniqueAssetPath(projectDir, assetPath) {
|
|
|
3756
4212
|
const ext = extname2(assetPath);
|
|
3757
4213
|
const withoutExt = assetPath.slice(0, -ext.length);
|
|
3758
4214
|
let candidate = assetPath;
|
|
3759
|
-
for (let index = 2;
|
|
4215
|
+
for (let index = 2; existsSync10(join13(projectDir, candidate)); index++) {
|
|
3760
4216
|
candidate = `${withoutExt}-${index}${ext}`;
|
|
3761
4217
|
}
|
|
3762
4218
|
return candidate;
|
|
@@ -3807,7 +4263,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
3807
4263
|
}
|
|
3808
4264
|
const filePath = resolveWithinProject(project.dir, assetPath);
|
|
3809
4265
|
if (!filePath) return c.json({ error: "forbidden" }, 403);
|
|
3810
|
-
if (!
|
|
4266
|
+
if (!existsSync10(filePath)) return c.json({ error: "media not found" }, 404);
|
|
3811
4267
|
return c.json({ path: assetPath, metadata: await readMediaMetadata(filePath) });
|
|
3812
4268
|
});
|
|
3813
4269
|
api.post(
|
|
@@ -3829,7 +4285,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
3829
4285
|
}
|
|
3830
4286
|
const inputPath = resolveWithinProject(project.dir, inputAssetPath);
|
|
3831
4287
|
if (!inputPath) return c.json({ error: "forbidden" }, 403);
|
|
3832
|
-
if (!
|
|
4288
|
+
if (!existsSync10(inputPath)) return c.json({ error: "input media not found" }, 404);
|
|
3833
4289
|
const inputIsVideo = isVideoPath(inputAssetPath);
|
|
3834
4290
|
const inputIsImage = isImagePath(inputAssetPath);
|
|
3835
4291
|
if (!inputIsVideo && !inputIsImage) {
|
|
@@ -3863,8 +4319,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
3863
4319
|
return c.json({ error: "forbidden" }, 403);
|
|
3864
4320
|
}
|
|
3865
4321
|
}
|
|
3866
|
-
mkdirSync7(
|
|
3867
|
-
if (backgroundOutputPath) mkdirSync7(
|
|
4322
|
+
mkdirSync7(dirname3(outputPath), { recursive: true });
|
|
4323
|
+
if (backgroundOutputPath) mkdirSync7(dirname3(backgroundOutputPath), { recursive: true });
|
|
3868
4324
|
const jobId = makeJobId(project.id, mediaJobs);
|
|
3869
4325
|
const state = adapter.startBackgroundRemoval({
|
|
3870
4326
|
project,
|
|
@@ -3921,14 +4377,14 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
3921
4377
|
}
|
|
3922
4378
|
|
|
3923
4379
|
// src/routes/globalAssets.ts
|
|
3924
|
-
import { existsSync as
|
|
4380
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
|
|
3925
4381
|
import { homedir } from "os";
|
|
3926
4382
|
import { join as join14 } from "path";
|
|
3927
4383
|
function readGlobalAssets(home = homedir()) {
|
|
3928
4384
|
const manifestPath = join14(home, ".media", "manifest.jsonl");
|
|
3929
|
-
if (!
|
|
4385
|
+
if (!existsSync11(manifestPath)) return [];
|
|
3930
4386
|
const out = [];
|
|
3931
|
-
for (const line of
|
|
4387
|
+
for (const line of readFileSync13(manifestPath, "utf8").split("\n")) {
|
|
3932
4388
|
if (!line.trim()) continue;
|
|
3933
4389
|
try {
|
|
3934
4390
|
const rec = JSON.parse(line);
|