@hyperframes/studio-server 0.7.57 → 0.7.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +364 -220
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -31,6 +31,153 @@ function walkDir(dir, prefix = "") {
|
|
|
31
31
|
return files;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// src/helpers/projectSignature.ts
|
|
35
|
+
import { createHash } from "crypto";
|
|
36
|
+
import { lstatSync, readFileSync, readdirSync as readdirSync2 } from "fs";
|
|
37
|
+
import { extname, isAbsolute, relative, resolve } from "path";
|
|
38
|
+
var SIGNATURE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
39
|
+
".cjs",
|
|
40
|
+
".css",
|
|
41
|
+
".html",
|
|
42
|
+
".js",
|
|
43
|
+
".json",
|
|
44
|
+
".jsx",
|
|
45
|
+
".mjs",
|
|
46
|
+
".svg",
|
|
47
|
+
".ts",
|
|
48
|
+
".tsx"
|
|
49
|
+
]);
|
|
50
|
+
var SIGNATURE_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
51
|
+
".cache",
|
|
52
|
+
".git",
|
|
53
|
+
".hyperframes",
|
|
54
|
+
".next",
|
|
55
|
+
".vite",
|
|
56
|
+
"build",
|
|
57
|
+
"coverage",
|
|
58
|
+
"dist",
|
|
59
|
+
"node_modules",
|
|
60
|
+
"outputs",
|
|
61
|
+
"renders"
|
|
62
|
+
]);
|
|
63
|
+
var MAX_SIGNATURE_TEXT_BYTES = 2e6;
|
|
64
|
+
var STUDIO_SIGNATURE_MANIFEST_PATHS = [
|
|
65
|
+
".hyperframes/studio-manual-edits.json",
|
|
66
|
+
".hyperframes/studio-motion.json"
|
|
67
|
+
];
|
|
68
|
+
var projectSignatureCache = /* @__PURE__ */ new Map();
|
|
69
|
+
function isPathWithin(parentDir, childPath) {
|
|
70
|
+
const childRelativePath = relative(parentDir, childPath);
|
|
71
|
+
return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute(childRelativePath);
|
|
72
|
+
}
|
|
73
|
+
function isTextContentEligible(file, size) {
|
|
74
|
+
return SIGNATURE_TEXT_EXTENSIONS.has(extname(file).toLowerCase()) && size <= MAX_SIGNATURE_TEXT_BYTES;
|
|
75
|
+
}
|
|
76
|
+
function collectProjectSignatureFiles(projectDir, dir, files) {
|
|
77
|
+
let entries;
|
|
78
|
+
try {
|
|
79
|
+
entries = readdirSync2(dir).sort();
|
|
80
|
+
} catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
if (SIGNATURE_EXCLUDED_DIRS.has(entry)) continue;
|
|
85
|
+
const file = resolve(dir, entry);
|
|
86
|
+
if (!isPathWithin(projectDir, file)) continue;
|
|
87
|
+
let stat;
|
|
88
|
+
try {
|
|
89
|
+
stat = lstatSync(file);
|
|
90
|
+
} catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (stat.isSymbolicLink()) continue;
|
|
94
|
+
if (stat.isDirectory()) {
|
|
95
|
+
collectProjectSignatureFiles(projectDir, file, files);
|
|
96
|
+
} else if (stat.isFile()) {
|
|
97
|
+
files.push({
|
|
98
|
+
file,
|
|
99
|
+
mtimeMs: stat.mtimeMs,
|
|
100
|
+
size: stat.size,
|
|
101
|
+
textContentEligible: isTextContentEligible(file, stat.size)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function collectProjectSignatureManifestFiles(projectDir, files) {
|
|
107
|
+
const seen = new Set(files.map((entry) => entry.file));
|
|
108
|
+
for (const manifestPath of STUDIO_SIGNATURE_MANIFEST_PATHS) {
|
|
109
|
+
const file = resolve(projectDir, manifestPath);
|
|
110
|
+
if (seen.has(file) || !isPathWithin(projectDir, file)) continue;
|
|
111
|
+
let stat;
|
|
112
|
+
try {
|
|
113
|
+
stat = lstatSync(file);
|
|
114
|
+
} catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (stat.isSymbolicLink() || !stat.isFile()) continue;
|
|
118
|
+
files.push({
|
|
119
|
+
file,
|
|
120
|
+
mtimeMs: stat.mtimeMs,
|
|
121
|
+
size: stat.size,
|
|
122
|
+
textContentEligible: isTextContentEligible(file, stat.size)
|
|
123
|
+
});
|
|
124
|
+
seen.add(file);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function createProjectFingerprint(projectDir, files) {
|
|
128
|
+
const hash = createHash("sha256");
|
|
129
|
+
for (const entry of files) {
|
|
130
|
+
hash.update(relative(projectDir, entry.file));
|
|
131
|
+
hash.update("\0");
|
|
132
|
+
hash.update(String(entry.size));
|
|
133
|
+
hash.update("\0");
|
|
134
|
+
hash.update(String(entry.mtimeMs));
|
|
135
|
+
hash.update("\0");
|
|
136
|
+
hash.update(entry.textContentEligible ? "text" : "binary");
|
|
137
|
+
hash.update("\0");
|
|
138
|
+
}
|
|
139
|
+
return hash.digest("hex").slice(0, 24);
|
|
140
|
+
}
|
|
141
|
+
function resolveProjectSignature(adapter, projectDir) {
|
|
142
|
+
return adapter.getProjectSignature?.(projectDir) ?? createProjectSignature(projectDir);
|
|
143
|
+
}
|
|
144
|
+
async function resolveProjectAndSignature(adapter, projectId) {
|
|
145
|
+
const project = await adapter.resolveProject(projectId);
|
|
146
|
+
if (!project) return null;
|
|
147
|
+
return { project, signature: resolveProjectSignature(adapter, project.dir) };
|
|
148
|
+
}
|
|
149
|
+
function createProjectSignature(projectDir) {
|
|
150
|
+
const normalizedProjectDir = resolve(projectDir);
|
|
151
|
+
const files = [];
|
|
152
|
+
collectProjectSignatureFiles(normalizedProjectDir, normalizedProjectDir, files);
|
|
153
|
+
collectProjectSignatureManifestFiles(normalizedProjectDir, files);
|
|
154
|
+
files.sort((a, b) => a.file.localeCompare(b.file));
|
|
155
|
+
const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
|
|
156
|
+
const cached = projectSignatureCache.get(normalizedProjectDir);
|
|
157
|
+
if (cached?.fingerprint === fingerprint) return cached.signature;
|
|
158
|
+
const hash = createHash("sha256");
|
|
159
|
+
for (const entry of files) {
|
|
160
|
+
const relativePath = relative(normalizedProjectDir, entry.file);
|
|
161
|
+
hash.update(relativePath);
|
|
162
|
+
hash.update("\0");
|
|
163
|
+
hash.update(String(entry.size));
|
|
164
|
+
hash.update("\0");
|
|
165
|
+
if (entry.textContentEligible) {
|
|
166
|
+
try {
|
|
167
|
+
hash.update(readFileSync(entry.file));
|
|
168
|
+
} catch {
|
|
169
|
+
hash.update(String(entry.mtimeMs));
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
hash.update(String(entry.mtimeMs));
|
|
173
|
+
}
|
|
174
|
+
hash.update("\0");
|
|
175
|
+
}
|
|
176
|
+
const signature = hash.digest("hex").slice(0, 24);
|
|
177
|
+
projectSignatureCache.set(normalizedProjectDir, { fingerprint, signature });
|
|
178
|
+
return signature;
|
|
179
|
+
}
|
|
180
|
+
|
|
34
181
|
// src/routes/projects.ts
|
|
35
182
|
var COMPOSITION_ID_RE = /data-composition-id\s*=/;
|
|
36
183
|
async function filterCompositionFiles(projectDir, files) {
|
|
@@ -61,6 +208,11 @@ function registerProjectRoutes(api, adapter) {
|
|
|
61
208
|
if (!result) return c.json({ error: "Session not found" }, 404);
|
|
62
209
|
return c.json(result);
|
|
63
210
|
});
|
|
211
|
+
api.get("/projects/:id/signature", async (c) => {
|
|
212
|
+
const project = await adapter.resolveProject(c.req.param("id"));
|
|
213
|
+
if (!project) return c.json({ error: "not found" }, 404);
|
|
214
|
+
return c.json({ signature: resolveProjectSignature(adapter, project.dir) });
|
|
215
|
+
});
|
|
64
216
|
api.get("/projects/:id", async (c) => {
|
|
65
217
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
66
218
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
@@ -71,7 +223,7 @@ function registerProjectRoutes(api, adapter) {
|
|
|
71
223
|
}
|
|
72
224
|
|
|
73
225
|
// src/routes/storyboard.ts
|
|
74
|
-
import { existsSync, readFileSync } from "fs";
|
|
226
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
75
227
|
import {
|
|
76
228
|
parseStoryboard,
|
|
77
229
|
SCRIPT_FILENAME,
|
|
@@ -91,7 +243,7 @@ function readScript(projectDir) {
|
|
|
91
243
|
const abs = resolveWithinProject(projectDir, SCRIPT_FILENAME);
|
|
92
244
|
if (abs && existsSync(abs)) {
|
|
93
245
|
try {
|
|
94
|
-
return { exists: true, path: SCRIPT_FILENAME, content:
|
|
246
|
+
return { exists: true, path: SCRIPT_FILENAME, content: readFileSync2(abs, "utf-8") };
|
|
95
247
|
} catch {
|
|
96
248
|
}
|
|
97
249
|
}
|
|
@@ -99,8 +251,9 @@ function readScript(projectDir) {
|
|
|
99
251
|
}
|
|
100
252
|
function registerStoryboardRoutes(api, adapter) {
|
|
101
253
|
api.get("/projects/:id/storyboard", async (c) => {
|
|
102
|
-
const
|
|
103
|
-
if (!
|
|
254
|
+
const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
|
|
255
|
+
if (!resolved) return c.json({ error: "not found" }, 404);
|
|
256
|
+
const { project, signature } = resolved;
|
|
104
257
|
const abs = resolveWithinProject(project.dir, STORYBOARD_FILENAME);
|
|
105
258
|
if (!abs || !existsSync(abs)) {
|
|
106
259
|
return c.json({
|
|
@@ -109,12 +262,13 @@ function registerStoryboardRoutes(api, adapter) {
|
|
|
109
262
|
globals: { extra: {} },
|
|
110
263
|
frames: [],
|
|
111
264
|
warnings: [],
|
|
112
|
-
script: readScript(project.dir)
|
|
265
|
+
script: readScript(project.dir),
|
|
266
|
+
signature
|
|
113
267
|
});
|
|
114
268
|
}
|
|
115
269
|
let source;
|
|
116
270
|
try {
|
|
117
|
-
source =
|
|
271
|
+
source = readFileSync2(abs, "utf-8");
|
|
118
272
|
} catch {
|
|
119
273
|
return c.json({ error: "failed to read storyboard" }, 500);
|
|
120
274
|
}
|
|
@@ -125,7 +279,8 @@ function registerStoryboardRoutes(api, adapter) {
|
|
|
125
279
|
globals: manifest.globals,
|
|
126
280
|
frames: resolveFrames(project.dir, manifest.frames),
|
|
127
281
|
warnings: manifest.warnings,
|
|
128
|
-
script: readScript(project.dir)
|
|
282
|
+
script: readScript(project.dir),
|
|
283
|
+
signature
|
|
129
284
|
});
|
|
130
285
|
});
|
|
131
286
|
}
|
|
@@ -134,16 +289,16 @@ function registerStoryboardRoutes(api, adapter) {
|
|
|
134
289
|
import { bodyLimit } from "hono/body-limit";
|
|
135
290
|
import {
|
|
136
291
|
existsSync as existsSync3,
|
|
137
|
-
readFileSync as
|
|
292
|
+
readFileSync as readFileSync4,
|
|
138
293
|
writeFileSync as writeFileSync4,
|
|
139
294
|
mkdirSync as mkdirSync3,
|
|
140
295
|
unlinkSync as unlinkSync2,
|
|
141
296
|
rmSync as rmSync2,
|
|
142
297
|
statSync,
|
|
143
298
|
renameSync,
|
|
144
|
-
readdirSync as
|
|
299
|
+
readdirSync as readdirSync4
|
|
145
300
|
} from "fs";
|
|
146
|
-
import { resolve as
|
|
301
|
+
import { resolve as resolve3, dirname, join as join6 } from "path";
|
|
147
302
|
|
|
148
303
|
// src/helpers/mime.ts
|
|
149
304
|
var MIME_TYPES = {
|
|
@@ -188,7 +343,7 @@ function isAudioFile(name) {
|
|
|
188
343
|
// src/helpers/waveform.ts
|
|
189
344
|
import { spawn } from "child_process";
|
|
190
345
|
import { existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
|
|
191
|
-
import { join as join3, resolve } from "path";
|
|
346
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
192
347
|
var SAMPLE_RATE = 4e3;
|
|
193
348
|
var PEAK_COUNT = 4e3;
|
|
194
349
|
var WAVEFORM_CACHE_VERSION = "v2";
|
|
@@ -213,7 +368,7 @@ function computePeaks(floats, count) {
|
|
|
213
368
|
}
|
|
214
369
|
function ffmpegBinary() {
|
|
215
370
|
const configured = process.env.HYPERFRAMES_FFMPEG_PATH?.trim();
|
|
216
|
-
if (configured) return
|
|
371
|
+
if (configured) return resolve2(configured);
|
|
217
372
|
return "ffmpeg";
|
|
218
373
|
}
|
|
219
374
|
function decodeAudioPeaks(audioPath) {
|
|
@@ -322,9 +477,9 @@ function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
|
|
|
322
477
|
}
|
|
323
478
|
|
|
324
479
|
// src/helpers/backupJournal.ts
|
|
325
|
-
import { mkdirSync as mkdirSync2, readdirSync as
|
|
480
|
+
import { mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
326
481
|
import { Buffer as Buffer2 } from "buffer";
|
|
327
|
-
import { join as join5, relative } from "path";
|
|
482
|
+
import { join as join5, relative as relative2 } from "path";
|
|
328
483
|
var DEFAULT_KEEP_PER_FILE = 10;
|
|
329
484
|
function backupKeyForPath(path) {
|
|
330
485
|
return Buffer2.from(path, "utf-8").toString("base64url");
|
|
@@ -334,15 +489,15 @@ function timestampPrefix() {
|
|
|
334
489
|
}
|
|
335
490
|
function backupPathForResponse(projectDir, backupPath) {
|
|
336
491
|
if (!backupPath) return null;
|
|
337
|
-
const rel =
|
|
492
|
+
const rel = relative2(projectDir, backupPath);
|
|
338
493
|
if (!rel || rel.startsWith("..")) return null;
|
|
339
494
|
return rel.split("\\").join("/");
|
|
340
495
|
}
|
|
341
496
|
function snapshotBeforeWrite(projectDir, absPath, options = {}) {
|
|
342
497
|
if (!isSafePath(projectDir, absPath)) return { backupPath: null };
|
|
343
498
|
try {
|
|
344
|
-
const content =
|
|
345
|
-
const relativePath =
|
|
499
|
+
const content = readFileSync3(absPath);
|
|
500
|
+
const relativePath = relative2(projectDir, absPath);
|
|
346
501
|
const backupDir = join5(projectDir, ".hyperframes", "backup");
|
|
347
502
|
mkdirSync2(backupDir, { recursive: true });
|
|
348
503
|
const backupKey = backupKeyForPath(relativePath);
|
|
@@ -363,7 +518,7 @@ function nextBackupPath(backupDir, backupKey) {
|
|
|
363
518
|
let counter = 2;
|
|
364
519
|
while (true) {
|
|
365
520
|
try {
|
|
366
|
-
|
|
521
|
+
readFileSync3(candidate);
|
|
367
522
|
} catch (error) {
|
|
368
523
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
369
524
|
return candidate;
|
|
@@ -378,7 +533,7 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
|
|
|
378
533
|
const keep = Math.max(1, Math.floor(keepPerFile));
|
|
379
534
|
const suffix = `-${backupKey}`;
|
|
380
535
|
const numberedSuffix = new RegExp(`-${backupKey}-\\d+$`);
|
|
381
|
-
const matches =
|
|
536
|
+
const matches = readdirSync3(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join5(backupDir, name)).sort((a, b) => {
|
|
382
537
|
return b.localeCompare(a);
|
|
383
538
|
});
|
|
384
539
|
for (const file of matches.slice(keep)) {
|
|
@@ -896,6 +1051,21 @@ function resolveProjectFile(c, adapter, opts) {
|
|
|
896
1051
|
function resolveFileMutationContext(c, adapter, operation) {
|
|
897
1052
|
return resolveProjectPath(c, adapter, (id) => `/projects/${id}/file-mutations/${operation}/`);
|
|
898
1053
|
}
|
|
1054
|
+
function isElementPatchRequest(value) {
|
|
1055
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1056
|
+
if (!("target" in value) || typeof value.target !== "object" || value.target === null) {
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
return "operations" in value && Array.isArray(value.operations) && value.operations.length > 0;
|
|
1060
|
+
}
|
|
1061
|
+
function isElementPatchBatchRequest(value) {
|
|
1062
|
+
return typeof value === "object" && value !== null && "sourceFile" in value && typeof value.sourceFile === "string" && value.sourceFile.length > 0 && "patches" in value && Array.isArray(value.patches) && value.patches.length > 0 && value.patches.every(isElementPatchRequest);
|
|
1063
|
+
}
|
|
1064
|
+
function findUnsafeElementPatchBatchValues(batches) {
|
|
1065
|
+
return batches.flatMap(
|
|
1066
|
+
(batch) => batch.patches.flatMap((patch) => findUnsafeDomPatchValues(patch))
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
899
1069
|
function foldElementPatches(originalContent, patches) {
|
|
900
1070
|
let content = originalContent;
|
|
901
1071
|
const matched = [];
|
|
@@ -906,6 +1076,90 @@ function foldElementPatches(originalContent, patches) {
|
|
|
906
1076
|
}
|
|
907
1077
|
return { content, matched };
|
|
908
1078
|
}
|
|
1079
|
+
function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSync4) {
|
|
1080
|
+
const resolvedPaths = /* @__PURE__ */ new Set();
|
|
1081
|
+
const prepared = [];
|
|
1082
|
+
for (const batch of batches) {
|
|
1083
|
+
const absPath = resolveWithinProject(projectDir, batch.sourceFile);
|
|
1084
|
+
if (!absPath) return { error: "forbidden", sourceFile: batch.sourceFile };
|
|
1085
|
+
if (resolvedPaths.has(absPath)) return { error: "duplicate", sourceFile: batch.sourceFile };
|
|
1086
|
+
resolvedPaths.add(absPath);
|
|
1087
|
+
let before;
|
|
1088
|
+
try {
|
|
1089
|
+
before = readFileSync4(absPath, "utf-8");
|
|
1090
|
+
} catch {
|
|
1091
|
+
return { error: "not-found", sourceFile: batch.sourceFile };
|
|
1092
|
+
}
|
|
1093
|
+
const folded = foldElementPatches(before, batch.patches);
|
|
1094
|
+
prepared.push({
|
|
1095
|
+
sourceFile: batch.sourceFile,
|
|
1096
|
+
absPath,
|
|
1097
|
+
before,
|
|
1098
|
+
matched: folded.matched,
|
|
1099
|
+
after: folded.content
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
const durable = prepared.every((file) => file.matched.every(Boolean));
|
|
1103
|
+
if (!durable) {
|
|
1104
|
+
return {
|
|
1105
|
+
durable: false,
|
|
1106
|
+
files: prepared.map((file) => ({
|
|
1107
|
+
sourceFile: file.sourceFile,
|
|
1108
|
+
changed: false,
|
|
1109
|
+
matched: file.matched,
|
|
1110
|
+
before: file.before,
|
|
1111
|
+
after: file.before
|
|
1112
|
+
}))
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
const files = [];
|
|
1116
|
+
const attemptedWrites = [];
|
|
1117
|
+
try {
|
|
1118
|
+
for (const file of prepared) {
|
|
1119
|
+
if (file.after === file.before) {
|
|
1120
|
+
files.push({
|
|
1121
|
+
sourceFile: file.sourceFile,
|
|
1122
|
+
changed: false,
|
|
1123
|
+
matched: file.matched,
|
|
1124
|
+
before: file.before,
|
|
1125
|
+
after: file.before
|
|
1126
|
+
});
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
const backup = snapshotBeforeWrite(projectDir, file.absPath);
|
|
1130
|
+
if (backup.error) {
|
|
1131
|
+
throw new Error(`Failed to create backup for ${file.sourceFile}: ${backup.error}`);
|
|
1132
|
+
}
|
|
1133
|
+
attemptedWrites.push(file);
|
|
1134
|
+
writeFile(file.absPath, file.after, "utf-8");
|
|
1135
|
+
files.push({
|
|
1136
|
+
sourceFile: file.sourceFile,
|
|
1137
|
+
changed: true,
|
|
1138
|
+
matched: file.matched,
|
|
1139
|
+
before: file.before,
|
|
1140
|
+
after: file.after,
|
|
1141
|
+
backupPath: backupPathForResponse(projectDir, backup.backupPath)
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
const rollbackErrors = [];
|
|
1146
|
+
for (const file of attemptedWrites.reverse()) {
|
|
1147
|
+
try {
|
|
1148
|
+
writeFile(file.absPath, file.before, "utf-8");
|
|
1149
|
+
} catch (rollbackError) {
|
|
1150
|
+
rollbackErrors.push(rollbackError);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (rollbackErrors.length > 0) {
|
|
1154
|
+
throw new AggregateError(
|
|
1155
|
+
[error, ...rollbackErrors],
|
|
1156
|
+
"Element patch batch failed and rollback did not complete"
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
throw error;
|
|
1160
|
+
}
|
|
1161
|
+
return { durable: true, files };
|
|
1162
|
+
}
|
|
909
1163
|
function writeIfChanged(c, projectDir, filePath, absPath, original, next) {
|
|
910
1164
|
if (next === original) {
|
|
911
1165
|
return c.json({ ok: true, changed: false, content: original, path: filePath });
|
|
@@ -931,6 +1185,11 @@ function rejectUnsafeMutationValues(c, unsafeFields) {
|
|
|
931
1185
|
400
|
|
932
1186
|
);
|
|
933
1187
|
}
|
|
1188
|
+
function elementPatchBatchCommitErrorResponse(c, error, sourceFile) {
|
|
1189
|
+
if (error === "not-found") return c.json({ error, sourceFile }, 404);
|
|
1190
|
+
if (error === "forbidden") return c.json({ error, sourceFile }, 403);
|
|
1191
|
+
return c.json({ error: "duplicate source file", sourceFile }, 400);
|
|
1192
|
+
}
|
|
934
1193
|
async function parseMutationBody(c) {
|
|
935
1194
|
const body = await c.req.json().catch(() => null);
|
|
936
1195
|
if (!body?.target) {
|
|
@@ -949,7 +1208,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
949
1208
|
const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
|
|
950
1209
|
let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
|
|
951
1210
|
let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
|
|
952
|
-
while (existsSync3(
|
|
1211
|
+
while (existsSync3(resolve3(projectDir, candidate))) {
|
|
953
1212
|
num++;
|
|
954
1213
|
candidate = `${cleanBase} (copy ${num})${ext}`;
|
|
955
1214
|
}
|
|
@@ -957,7 +1216,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
957
1216
|
}
|
|
958
1217
|
function walkFiles(dir, filter) {
|
|
959
1218
|
const results = [];
|
|
960
|
-
for (const entry of
|
|
1219
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
961
1220
|
const full = join6(dir, entry.name);
|
|
962
1221
|
if (entry.isDirectory()) {
|
|
963
1222
|
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders")
|
|
@@ -976,7 +1235,7 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
976
1235
|
);
|
|
977
1236
|
let updatedCount = 0;
|
|
978
1237
|
for (const file of textFiles) {
|
|
979
|
-
const content =
|
|
1238
|
+
const content = readFileSync4(file, "utf-8");
|
|
980
1239
|
if (!content.includes(oldPath)) continue;
|
|
981
1240
|
const updated = content.split(oldPath).join(newPath);
|
|
982
1241
|
if (updated !== content) {
|
|
@@ -1233,7 +1492,8 @@ function validateGsapMutationRequest(c, body) {
|
|
|
1233
1492
|
return null;
|
|
1234
1493
|
}
|
|
1235
1494
|
async function prepareGsapMutationScript(c, res, firstMutation) {
|
|
1236
|
-
|
|
1495
|
+
const beforeHtml = readFileSync4(res.absPath, "utf-8");
|
|
1496
|
+
let html = beforeHtml;
|
|
1237
1497
|
let block = extractGsapScriptBlock(html);
|
|
1238
1498
|
if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
|
|
1239
1499
|
const compId = html.match(/data-composition-id="([^"]+)"/)?.[1] ?? "main";
|
|
@@ -1265,14 +1525,14 @@ ${bootstrap}`;
|
|
|
1265
1525
|
});
|
|
1266
1526
|
}
|
|
1267
1527
|
if (!block) return c.json({ error: "no GSAP script found in file" }, 400);
|
|
1268
|
-
return { html, block };
|
|
1528
|
+
return { html, beforeHtml, block };
|
|
1269
1529
|
}
|
|
1270
1530
|
async function applyGsapMutations(c, res, mutations) {
|
|
1271
1531
|
const firstMutation = mutations[0];
|
|
1272
1532
|
if (!firstMutation) return c.json({ error: "mutations array required" }, 400);
|
|
1273
1533
|
const prepared = await prepareGsapMutationScript(c, res, firstMutation);
|
|
1274
1534
|
if (prepared instanceof Response) return prepared;
|
|
1275
|
-
const { html, block } = prepared;
|
|
1535
|
+
const { html, beforeHtml, block } = prepared;
|
|
1276
1536
|
const initialScript = block.scriptText;
|
|
1277
1537
|
const skippedSelectors = /* @__PURE__ */ new Set();
|
|
1278
1538
|
const respond = (data, status) => status ? c.json(data, status) : c.json(data);
|
|
@@ -1292,6 +1552,9 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1292
1552
|
const changed = block.scriptText !== initialScript;
|
|
1293
1553
|
const newHtml = changed ? block.replaceScript(block.scriptText) : html;
|
|
1294
1554
|
let backupPath = null;
|
|
1555
|
+
if (readFileSync4(res.absPath, "utf-8") !== beforeHtml) {
|
|
1556
|
+
return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
|
|
1557
|
+
}
|
|
1295
1558
|
if (changed) {
|
|
1296
1559
|
const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
1297
1560
|
if (backup.error) console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
|
|
@@ -1303,7 +1566,7 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1303
1566
|
changed,
|
|
1304
1567
|
mutated: changed,
|
|
1305
1568
|
parsed: parseGsapScriptAcorn(block.scriptText),
|
|
1306
|
-
before:
|
|
1569
|
+
before: beforeHtml,
|
|
1307
1570
|
after: newHtml,
|
|
1308
1571
|
scriptText: block.scriptText,
|
|
1309
1572
|
path: res.filePath,
|
|
@@ -1925,7 +2188,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
1925
2188
|
skipped.push(name);
|
|
1926
2189
|
continue;
|
|
1927
2190
|
}
|
|
1928
|
-
const destPath =
|
|
2191
|
+
const destPath = resolve3(targetDir, name);
|
|
1929
2192
|
if (!isSafePath(projectDir, destPath)) continue;
|
|
1930
2193
|
let finalPath = destPath;
|
|
1931
2194
|
let finalName = name;
|
|
@@ -1935,13 +2198,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
1935
2198
|
const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
|
1936
2199
|
let n = 2;
|
|
1937
2200
|
const MAX_COPY_INDEX = 1e4;
|
|
1938
|
-
while (n < MAX_COPY_INDEX && existsSync3(
|
|
2201
|
+
while (n < MAX_COPY_INDEX && existsSync3(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
|
|
1939
2202
|
if (n >= MAX_COPY_INDEX) {
|
|
1940
2203
|
skipped.push(name);
|
|
1941
2204
|
continue;
|
|
1942
2205
|
}
|
|
1943
2206
|
finalName = `${base} (${n})${ext}`;
|
|
1944
|
-
finalPath =
|
|
2207
|
+
finalPath = resolve3(targetDir, finalName);
|
|
1945
2208
|
}
|
|
1946
2209
|
const buffer = Buffer.from(await value.arrayBuffer());
|
|
1947
2210
|
const validation = validateUploadedMediaBuffer(finalName, buffer);
|
|
@@ -1969,7 +2232,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
1969
2232
|
}
|
|
1970
2233
|
return c.json({ error: "not found" }, 404);
|
|
1971
2234
|
}
|
|
1972
|
-
const content =
|
|
2235
|
+
const content = readFileSync4(res.absPath, "utf-8");
|
|
1973
2236
|
return c.json({ filename: res.filePath, content });
|
|
1974
2237
|
});
|
|
1975
2238
|
api.put("/projects/:id/files/*", async (c) => {
|
|
@@ -2021,7 +2284,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2021
2284
|
}
|
|
2022
2285
|
const parsed = await parseMutationBody(c);
|
|
2023
2286
|
if ("error" in parsed) return parsed.error;
|
|
2024
|
-
const originalContent =
|
|
2287
|
+
const originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2025
2288
|
return writeIfChanged(
|
|
2026
2289
|
c,
|
|
2027
2290
|
ctx.project.dir,
|
|
@@ -2042,7 +2305,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2042
2305
|
const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
|
|
2043
2306
|
let originalContent;
|
|
2044
2307
|
try {
|
|
2045
|
-
originalContent =
|
|
2308
|
+
originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2046
2309
|
} catch {
|
|
2047
2310
|
return c.json({ error: "not found" }, 404);
|
|
2048
2311
|
}
|
|
@@ -2082,7 +2345,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2082
2345
|
}
|
|
2083
2346
|
let originalContent;
|
|
2084
2347
|
try {
|
|
2085
|
-
originalContent =
|
|
2348
|
+
originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2086
2349
|
} catch {
|
|
2087
2350
|
return c.json({ error: "not found" }, 404);
|
|
2088
2351
|
}
|
|
@@ -2112,45 +2375,46 @@ function registerFileRoutes(api, adapter) {
|
|
|
2112
2375
|
backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
|
|
2113
2376
|
});
|
|
2114
2377
|
});
|
|
2378
|
+
api.post("/projects/:id/file-mutations/patch-element-batches", async (c) => {
|
|
2379
|
+
const project = await adapter.resolveProject(c.req.param("id"));
|
|
2380
|
+
if (!project) return c.json({ error: "not found" }, 404);
|
|
2381
|
+
const body = await c.req.json().catch(() => null);
|
|
2382
|
+
if (typeof body !== "object" || body === null || !("batches" in body) || !Array.isArray(body.batches) || body.batches.length === 0 || !body.batches.every(isElementPatchBatchRequest)) {
|
|
2383
|
+
return c.json({ error: "batches with sourceFile and patches required" }, 400);
|
|
2384
|
+
}
|
|
2385
|
+
const unsafeFields = findUnsafeElementPatchBatchValues(body.batches);
|
|
2386
|
+
if (unsafeFields.length > 0) return rejectUnsafeMutationValues(c, unsafeFields);
|
|
2387
|
+
const result = commitElementPatchBatches(project.dir, body.batches);
|
|
2388
|
+
if ("error" in result) {
|
|
2389
|
+
return elementPatchBatchCommitErrorResponse(c, result.error, result.sourceFile);
|
|
2390
|
+
}
|
|
2391
|
+
return c.json(result);
|
|
2392
|
+
});
|
|
2115
2393
|
api.post("/projects/:id/file-mutations/patch-elements-batch/*", async (c) => {
|
|
2116
2394
|
const ctx = await resolveFileMutationContext(c, adapter, "patch-elements-batch");
|
|
2117
2395
|
if ("error" in ctx) return ctx.error;
|
|
2118
2396
|
const body = await c.req.json().catch(() => null);
|
|
2119
|
-
if (!body || !Array.isArray(body.patches) || body.patches.length === 0 || body.patches.
|
|
2120
|
-
(patch) => !patch?.target || !Array.isArray(patch.operations) || patch.operations.length === 0
|
|
2121
|
-
)) {
|
|
2397
|
+
if (!body || !Array.isArray(body.patches) || body.patches.length === 0 || !body.patches.every(isElementPatchRequest)) {
|
|
2122
2398
|
return c.json({ error: "patches with target and operations required" }, 400);
|
|
2123
2399
|
}
|
|
2124
|
-
const
|
|
2400
|
+
const batch = { sourceFile: ctx.filePath, patches: body.patches };
|
|
2401
|
+
const unsafeFields = findUnsafeElementPatchBatchValues([batch]);
|
|
2125
2402
|
if (unsafeFields.length > 0) {
|
|
2126
2403
|
return rejectUnsafeMutationValues(c, unsafeFields);
|
|
2127
2404
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
} catch {
|
|
2132
|
-
return c.json({ error: "not found" }, 404);
|
|
2133
|
-
}
|
|
2134
|
-
const result = foldElementPatches(originalContent, body.patches);
|
|
2135
|
-
if (result.content === originalContent) {
|
|
2136
|
-
return c.json({
|
|
2137
|
-
ok: true,
|
|
2138
|
-
changed: false,
|
|
2139
|
-
matched: result.matched,
|
|
2140
|
-
content: originalContent,
|
|
2141
|
-
path: ctx.filePath
|
|
2142
|
-
});
|
|
2405
|
+
const result = commitElementPatchBatches(ctx.project.dir, [batch]);
|
|
2406
|
+
if ("error" in result) {
|
|
2407
|
+
return elementPatchBatchCommitErrorResponse(c, result.error, result.sourceFile);
|
|
2143
2408
|
}
|
|
2144
|
-
const
|
|
2145
|
-
if (
|
|
2146
|
-
writeFileSync4(ctx.absPath, result.content, "utf-8");
|
|
2409
|
+
const file = result.files[0];
|
|
2410
|
+
if (!file) return c.json({ error: "empty element patch result" }, 500);
|
|
2147
2411
|
return c.json({
|
|
2148
2412
|
ok: true,
|
|
2149
|
-
changed:
|
|
2150
|
-
matched:
|
|
2151
|
-
content:
|
|
2152
|
-
path:
|
|
2153
|
-
backupPath:
|
|
2413
|
+
changed: file.changed,
|
|
2414
|
+
matched: file.matched,
|
|
2415
|
+
content: file.after,
|
|
2416
|
+
path: file.sourceFile,
|
|
2417
|
+
backupPath: file.backupPath
|
|
2154
2418
|
});
|
|
2155
2419
|
});
|
|
2156
2420
|
api.post("/projects/:id/file-mutations/wrap-elements/*", async (c) => {
|
|
@@ -2171,7 +2435,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2171
2435
|
}
|
|
2172
2436
|
let originalContent;
|
|
2173
2437
|
try {
|
|
2174
|
-
originalContent =
|
|
2438
|
+
originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2175
2439
|
} catch {
|
|
2176
2440
|
return c.json({ error: "not found" }, 404);
|
|
2177
2441
|
}
|
|
@@ -2213,7 +2477,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2213
2477
|
if ("error" in parsed) return parsed.error;
|
|
2214
2478
|
let originalContent;
|
|
2215
2479
|
try {
|
|
2216
|
-
originalContent =
|
|
2480
|
+
originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2217
2481
|
} catch {
|
|
2218
2482
|
return c.json({ error: "not found" }, 404);
|
|
2219
2483
|
}
|
|
@@ -2242,7 +2506,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2242
2506
|
if ("error" in parsed) return parsed.error;
|
|
2243
2507
|
let content;
|
|
2244
2508
|
try {
|
|
2245
|
-
content =
|
|
2509
|
+
content = readFileSync4(ctx.absPath, "utf-8");
|
|
2246
2510
|
} catch {
|
|
2247
2511
|
return c.json({ exists: false });
|
|
2248
2512
|
}
|
|
@@ -2285,7 +2549,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2285
2549
|
return c.json({ error: "forbidden" }, 403);
|
|
2286
2550
|
}
|
|
2287
2551
|
ensureDir(destAbs);
|
|
2288
|
-
writeFileSync4(destAbs,
|
|
2552
|
+
writeFileSync4(destAbs, readFileSync4(srcAbs));
|
|
2289
2553
|
return c.json({ ok: true, path: copyPath }, 201);
|
|
2290
2554
|
});
|
|
2291
2555
|
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
@@ -2315,7 +2579,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2315
2579
|
mustExist: true
|
|
2316
2580
|
});
|
|
2317
2581
|
if ("error" in res) return res.error;
|
|
2318
|
-
const html =
|
|
2582
|
+
const html = readFileSync4(res.absPath, "utf-8");
|
|
2319
2583
|
const block = extractGsapScriptBlock(html);
|
|
2320
2584
|
if (!block) {
|
|
2321
2585
|
return c.json({
|
|
@@ -2328,6 +2592,11 @@ function registerFileRoutes(api, adapter) {
|
|
|
2328
2592
|
const parsed = parseGsapScriptAcorn(block.scriptText);
|
|
2329
2593
|
return c.json(parsed);
|
|
2330
2594
|
});
|
|
2595
|
+
api.get("/projects/:id/gsap-mutation-capabilities", async (c) => {
|
|
2596
|
+
const project = await adapter.resolveProject(c.req.param("id"));
|
|
2597
|
+
if (!project) return c.json({ error: "not found" }, 404);
|
|
2598
|
+
return c.json({ atomicOwnershipPairs: true });
|
|
2599
|
+
});
|
|
2331
2600
|
api.post("/projects/:id/gsap-mutations/*", async (c) => {
|
|
2332
2601
|
const res = await resolveProjectPath(c, adapter, (id) => `/projects/${id}/gsap-mutations/`, {
|
|
2333
2602
|
mustExist: true
|
|
@@ -2357,6 +2626,25 @@ function registerFileRoutes(api, adapter) {
|
|
|
2357
2626
|
}
|
|
2358
2627
|
return applyGsapMutations(c, res, body.mutations);
|
|
2359
2628
|
});
|
|
2629
|
+
api.post("/projects/:id/gsap-mutation-rollback/*", async (c) => {
|
|
2630
|
+
const res = await resolveProjectPath(
|
|
2631
|
+
c,
|
|
2632
|
+
adapter,
|
|
2633
|
+
(id) => `/projects/${id}/gsap-mutation-rollback/`,
|
|
2634
|
+
{ mustExist: true }
|
|
2635
|
+
);
|
|
2636
|
+
if ("error" in res) return res.error;
|
|
2637
|
+
const body = await c.req.json().catch(() => null);
|
|
2638
|
+
if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
|
|
2639
|
+
return c.json({ error: "expected and restore contents required" }, 400);
|
|
2640
|
+
}
|
|
2641
|
+
const current = readFileSync4(res.absPath, "utf-8");
|
|
2642
|
+
if (current !== body.expected) {
|
|
2643
|
+
return c.json({ ok: true, restored: false, conflict: true });
|
|
2644
|
+
}
|
|
2645
|
+
writeFileSync4(res.absPath, body.restore, "utf-8");
|
|
2646
|
+
return c.json({ ok: true, restored: true, conflict: false });
|
|
2647
|
+
});
|
|
2360
2648
|
}
|
|
2361
2649
|
|
|
2362
2650
|
// src/routes/preview.ts
|
|
@@ -2366,7 +2654,7 @@ import { createHash as createHash2 } from "crypto";
|
|
|
2366
2654
|
import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
|
|
2367
2655
|
|
|
2368
2656
|
// src/helpers/subComposition.ts
|
|
2369
|
-
import { existsSync as existsSync4, readFileSync as
|
|
2657
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
2370
2658
|
import { join as join7 } from "path";
|
|
2371
2659
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
2372
2660
|
import {
|
|
@@ -2475,7 +2763,7 @@ function tagRootCompositionFile(bodyHtml, compPath) {
|
|
|
2475
2763
|
function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
|
|
2476
2764
|
const compFile = join7(projectDir, compPath);
|
|
2477
2765
|
if (!existsSync4(compFile)) return null;
|
|
2478
|
-
const rawComp = rawOverride ??
|
|
2766
|
+
const rawComp = rawOverride ?? readFileSync5(compFile, "utf-8");
|
|
2479
2767
|
let compHeadContent = "";
|
|
2480
2768
|
let rewrittenContent;
|
|
2481
2769
|
let htmlAttrs = "";
|
|
@@ -2508,7 +2796,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
2508
2796
|
const indexPath = join7(projectDir, "index.html");
|
|
2509
2797
|
let headContent = "";
|
|
2510
2798
|
if (existsSync4(indexPath)) {
|
|
2511
|
-
const indexHtml =
|
|
2799
|
+
const indexHtml = readFileSync5(indexPath, "utf-8");
|
|
2512
2800
|
const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
2513
2801
|
headContent = headMatch?.[1] ?? "";
|
|
2514
2802
|
}
|
|
@@ -2541,145 +2829,6 @@ ${rewrittenContent}
|
|
|
2541
2829
|
</html>`;
|
|
2542
2830
|
}
|
|
2543
2831
|
|
|
2544
|
-
// src/helpers/projectSignature.ts
|
|
2545
|
-
import { createHash } from "crypto";
|
|
2546
|
-
import { lstatSync, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
|
|
2547
|
-
import { extname, isAbsolute, relative as relative2, resolve as resolve3 } from "path";
|
|
2548
|
-
var SIGNATURE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2549
|
-
".cjs",
|
|
2550
|
-
".css",
|
|
2551
|
-
".html",
|
|
2552
|
-
".js",
|
|
2553
|
-
".json",
|
|
2554
|
-
".jsx",
|
|
2555
|
-
".mjs",
|
|
2556
|
-
".svg",
|
|
2557
|
-
".ts",
|
|
2558
|
-
".tsx"
|
|
2559
|
-
]);
|
|
2560
|
-
var SIGNATURE_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
2561
|
-
".cache",
|
|
2562
|
-
".git",
|
|
2563
|
-
".hyperframes",
|
|
2564
|
-
".next",
|
|
2565
|
-
".vite",
|
|
2566
|
-
"build",
|
|
2567
|
-
"coverage",
|
|
2568
|
-
"dist",
|
|
2569
|
-
"node_modules",
|
|
2570
|
-
"outputs",
|
|
2571
|
-
"renders"
|
|
2572
|
-
]);
|
|
2573
|
-
var MAX_SIGNATURE_TEXT_BYTES = 2e6;
|
|
2574
|
-
var STUDIO_SIGNATURE_MANIFEST_PATHS = [
|
|
2575
|
-
".hyperframes/studio-manual-edits.json",
|
|
2576
|
-
".hyperframes/studio-motion.json"
|
|
2577
|
-
];
|
|
2578
|
-
var projectSignatureCache = /* @__PURE__ */ new Map();
|
|
2579
|
-
function isPathWithin(parentDir, childPath) {
|
|
2580
|
-
const childRelativePath = relative2(parentDir, childPath);
|
|
2581
|
-
return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute(childRelativePath);
|
|
2582
|
-
}
|
|
2583
|
-
function isTextContentEligible(file, size) {
|
|
2584
|
-
return SIGNATURE_TEXT_EXTENSIONS.has(extname(file).toLowerCase()) && size <= MAX_SIGNATURE_TEXT_BYTES;
|
|
2585
|
-
}
|
|
2586
|
-
function collectProjectSignatureFiles(projectDir, dir, files) {
|
|
2587
|
-
let entries;
|
|
2588
|
-
try {
|
|
2589
|
-
entries = readdirSync4(dir).sort();
|
|
2590
|
-
} catch {
|
|
2591
|
-
return;
|
|
2592
|
-
}
|
|
2593
|
-
for (const entry of entries) {
|
|
2594
|
-
if (SIGNATURE_EXCLUDED_DIRS.has(entry)) continue;
|
|
2595
|
-
const file = resolve3(dir, entry);
|
|
2596
|
-
if (!isPathWithin(projectDir, file)) continue;
|
|
2597
|
-
let stat;
|
|
2598
|
-
try {
|
|
2599
|
-
stat = lstatSync(file);
|
|
2600
|
-
} catch {
|
|
2601
|
-
continue;
|
|
2602
|
-
}
|
|
2603
|
-
if (stat.isSymbolicLink()) continue;
|
|
2604
|
-
if (stat.isDirectory()) {
|
|
2605
|
-
collectProjectSignatureFiles(projectDir, file, files);
|
|
2606
|
-
} else if (stat.isFile()) {
|
|
2607
|
-
files.push({
|
|
2608
|
-
file,
|
|
2609
|
-
mtimeMs: stat.mtimeMs,
|
|
2610
|
-
size: stat.size,
|
|
2611
|
-
textContentEligible: isTextContentEligible(file, stat.size)
|
|
2612
|
-
});
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
function collectProjectSignatureManifestFiles(projectDir, files) {
|
|
2617
|
-
const seen = new Set(files.map((entry) => entry.file));
|
|
2618
|
-
for (const manifestPath of STUDIO_SIGNATURE_MANIFEST_PATHS) {
|
|
2619
|
-
const file = resolve3(projectDir, manifestPath);
|
|
2620
|
-
if (seen.has(file) || !isPathWithin(projectDir, file)) continue;
|
|
2621
|
-
let stat;
|
|
2622
|
-
try {
|
|
2623
|
-
stat = lstatSync(file);
|
|
2624
|
-
} catch {
|
|
2625
|
-
continue;
|
|
2626
|
-
}
|
|
2627
|
-
if (stat.isSymbolicLink() || !stat.isFile()) continue;
|
|
2628
|
-
files.push({
|
|
2629
|
-
file,
|
|
2630
|
-
mtimeMs: stat.mtimeMs,
|
|
2631
|
-
size: stat.size,
|
|
2632
|
-
textContentEligible: isTextContentEligible(file, stat.size)
|
|
2633
|
-
});
|
|
2634
|
-
seen.add(file);
|
|
2635
|
-
}
|
|
2636
|
-
}
|
|
2637
|
-
function createProjectFingerprint(projectDir, files) {
|
|
2638
|
-
const hash = createHash("sha256");
|
|
2639
|
-
for (const entry of files) {
|
|
2640
|
-
hash.update(relative2(projectDir, entry.file));
|
|
2641
|
-
hash.update("\0");
|
|
2642
|
-
hash.update(String(entry.size));
|
|
2643
|
-
hash.update("\0");
|
|
2644
|
-
hash.update(String(entry.mtimeMs));
|
|
2645
|
-
hash.update("\0");
|
|
2646
|
-
hash.update(entry.textContentEligible ? "text" : "binary");
|
|
2647
|
-
hash.update("\0");
|
|
2648
|
-
}
|
|
2649
|
-
return hash.digest("hex").slice(0, 24);
|
|
2650
|
-
}
|
|
2651
|
-
function createProjectSignature(projectDir) {
|
|
2652
|
-
const normalizedProjectDir = resolve3(projectDir);
|
|
2653
|
-
const files = [];
|
|
2654
|
-
collectProjectSignatureFiles(normalizedProjectDir, normalizedProjectDir, files);
|
|
2655
|
-
collectProjectSignatureManifestFiles(normalizedProjectDir, files);
|
|
2656
|
-
files.sort((a, b) => a.file.localeCompare(b.file));
|
|
2657
|
-
const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
|
|
2658
|
-
const cached = projectSignatureCache.get(normalizedProjectDir);
|
|
2659
|
-
if (cached?.fingerprint === fingerprint) return cached.signature;
|
|
2660
|
-
const hash = createHash("sha256");
|
|
2661
|
-
for (const entry of files) {
|
|
2662
|
-
const relativePath = relative2(normalizedProjectDir, entry.file);
|
|
2663
|
-
hash.update(relativePath);
|
|
2664
|
-
hash.update("\0");
|
|
2665
|
-
hash.update(String(entry.size));
|
|
2666
|
-
hash.update("\0");
|
|
2667
|
-
if (entry.textContentEligible) {
|
|
2668
|
-
try {
|
|
2669
|
-
hash.update(readFileSync5(entry.file));
|
|
2670
|
-
} catch {
|
|
2671
|
-
hash.update(String(entry.mtimeMs));
|
|
2672
|
-
}
|
|
2673
|
-
} else {
|
|
2674
|
-
hash.update(String(entry.mtimeMs));
|
|
2675
|
-
}
|
|
2676
|
-
hash.update("\0");
|
|
2677
|
-
}
|
|
2678
|
-
const signature = hash.digest("hex").slice(0, 24);
|
|
2679
|
-
projectSignatureCache.set(normalizedProjectDir, { fingerprint, signature });
|
|
2680
|
-
return signature;
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
2832
|
// src/helpers/studioMotionRenderScript.ts
|
|
2684
2833
|
var STUDIO_MOTION_PATH = ".hyperframes/studio-motion.json";
|
|
2685
2834
|
function hasStudioMotionEntries(manifestContent) {
|
|
@@ -2945,9 +3094,6 @@ var GSAP_CDN_VERSION = "3.15.0";
|
|
|
2945
3094
|
var GSAP_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/gsap.min.js"></script>`;
|
|
2946
3095
|
var GSAP_CUSTOM_EASE_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/CustomEase.min.js"></script>`;
|
|
2947
3096
|
var GSAP_MOTION_PATH_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/MotionPathPlugin.min.js"></script>`;
|
|
2948
|
-
function resolveProjectSignature(adapter, projectDir) {
|
|
2949
|
-
return adapter.getProjectSignature?.(projectDir) ?? createProjectSignature(projectDir);
|
|
2950
|
-
}
|
|
2951
3097
|
function injectProjectSignature(html, signature) {
|
|
2952
3098
|
const tag = `<meta name="${PROJECT_SIGNATURE_META}" content="${signature}">`;
|
|
2953
3099
|
if (html.includes(`name="${PROJECT_SIGNATURE_META}"`)) {
|
|
@@ -3136,12 +3282,12 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3136
3282
|
ETag: etag
|
|
3137
3283
|
});
|
|
3138
3284
|
api.get("/projects/:id/preview", async (c) => {
|
|
3139
|
-
const
|
|
3140
|
-
if (!
|
|
3285
|
+
const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
|
|
3286
|
+
if (!resolved) return c.json({ error: "not found" }, 404);
|
|
3287
|
+
const { project, signature } = resolved;
|
|
3141
3288
|
const vars = previewVariablesFromRequest(c.req.query("variables"));
|
|
3142
3289
|
if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
|
|
3143
3290
|
const previewVariables = vars.values;
|
|
3144
|
-
const signature = resolveProjectSignature(adapter, project.dir);
|
|
3145
3291
|
const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`;
|
|
3146
3292
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3147
3293
|
if (ifNoneMatch === etag) {
|
|
@@ -3201,12 +3347,12 @@ ${runtimeTag}`;
|
|
|
3201
3347
|
return stampFileHfIds(compFile);
|
|
3202
3348
|
}
|
|
3203
3349
|
api.get("/projects/:id/preview/comp/*", async (c) => {
|
|
3204
|
-
const
|
|
3205
|
-
if (!
|
|
3350
|
+
const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
|
|
3351
|
+
if (!resolved) return c.json({ error: "not found" }, 404);
|
|
3352
|
+
const { project, signature } = resolved;
|
|
3206
3353
|
const vars = previewVariablesFromRequest(c.req.query("variables"));
|
|
3207
3354
|
if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
|
|
3208
3355
|
const previewVariables = vars.values;
|
|
3209
|
-
const signature = resolveProjectSignature(adapter, project.dir);
|
|
3210
3356
|
const compPath = decodeURIComponent(
|
|
3211
3357
|
c.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? ""
|
|
3212
3358
|
);
|
|
@@ -3326,7 +3472,7 @@ import { streamSSE } from "hono/streaming";
|
|
|
3326
3472
|
import { existsSync as existsSync6, readFileSync as readFileSync9, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
3327
3473
|
import { join as join10 } from "path";
|
|
3328
3474
|
import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
|
|
3329
|
-
import { parseFps } from "@hyperframes/core";
|
|
3475
|
+
import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
|
|
3330
3476
|
var VALID_RESOLUTIONS = new Set(VALID_CANVAS_RESOLUTIONS);
|
|
3331
3477
|
function registerRenderRoutes(api, adapter) {
|
|
3332
3478
|
const renderJobs = /* @__PURE__ */ new Map();
|
|
@@ -3380,9 +3526,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3380
3526
|
variables = body.variables;
|
|
3381
3527
|
}
|
|
3382
3528
|
const now = /* @__PURE__ */ new Date();
|
|
3383
|
-
const
|
|
3384
|
-
const timePart = now.toTimeString().slice(0, 8).replace(/:/g, "-");
|
|
3385
|
-
const jobId = `${project.id}_${datePart}_${timePart}`;
|
|
3529
|
+
const jobId = `${project.id}_${formatRenderOutputTimestamp(now)}`;
|
|
3386
3530
|
const rendersDir = adapter.rendersDir(project);
|
|
3387
3531
|
if (!existsSync6(rendersDir)) mkdirSync4(rendersDir, { recursive: true });
|
|
3388
3532
|
const ext = FORMAT_EXT[format] ?? ".mp4";
|