@hyperframes/studio-server 0.7.58 → 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 +204 -193
- 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)) {
|
|
@@ -931,7 +1086,7 @@ function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSyn
|
|
|
931
1086
|
resolvedPaths.add(absPath);
|
|
932
1087
|
let before;
|
|
933
1088
|
try {
|
|
934
|
-
before =
|
|
1089
|
+
before = readFileSync4(absPath, "utf-8");
|
|
935
1090
|
} catch {
|
|
936
1091
|
return { error: "not-found", sourceFile: batch.sourceFile };
|
|
937
1092
|
}
|
|
@@ -1053,7 +1208,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1053
1208
|
const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
|
|
1054
1209
|
let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
|
|
1055
1210
|
let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
|
|
1056
|
-
while (existsSync3(
|
|
1211
|
+
while (existsSync3(resolve3(projectDir, candidate))) {
|
|
1057
1212
|
num++;
|
|
1058
1213
|
candidate = `${cleanBase} (copy ${num})${ext}`;
|
|
1059
1214
|
}
|
|
@@ -1061,7 +1216,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1061
1216
|
}
|
|
1062
1217
|
function walkFiles(dir, filter) {
|
|
1063
1218
|
const results = [];
|
|
1064
|
-
for (const entry of
|
|
1219
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
1065
1220
|
const full = join6(dir, entry.name);
|
|
1066
1221
|
if (entry.isDirectory()) {
|
|
1067
1222
|
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders")
|
|
@@ -1080,7 +1235,7 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
1080
1235
|
);
|
|
1081
1236
|
let updatedCount = 0;
|
|
1082
1237
|
for (const file of textFiles) {
|
|
1083
|
-
const content =
|
|
1238
|
+
const content = readFileSync4(file, "utf-8");
|
|
1084
1239
|
if (!content.includes(oldPath)) continue;
|
|
1085
1240
|
const updated = content.split(oldPath).join(newPath);
|
|
1086
1241
|
if (updated !== content) {
|
|
@@ -1337,7 +1492,7 @@ function validateGsapMutationRequest(c, body) {
|
|
|
1337
1492
|
return null;
|
|
1338
1493
|
}
|
|
1339
1494
|
async function prepareGsapMutationScript(c, res, firstMutation) {
|
|
1340
|
-
const beforeHtml =
|
|
1495
|
+
const beforeHtml = readFileSync4(res.absPath, "utf-8");
|
|
1341
1496
|
let html = beforeHtml;
|
|
1342
1497
|
let block = extractGsapScriptBlock(html);
|
|
1343
1498
|
if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
|
|
@@ -1397,7 +1552,7 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1397
1552
|
const changed = block.scriptText !== initialScript;
|
|
1398
1553
|
const newHtml = changed ? block.replaceScript(block.scriptText) : html;
|
|
1399
1554
|
let backupPath = null;
|
|
1400
|
-
if (
|
|
1555
|
+
if (readFileSync4(res.absPath, "utf-8") !== beforeHtml) {
|
|
1401
1556
|
return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
|
|
1402
1557
|
}
|
|
1403
1558
|
if (changed) {
|
|
@@ -2033,7 +2188,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2033
2188
|
skipped.push(name);
|
|
2034
2189
|
continue;
|
|
2035
2190
|
}
|
|
2036
|
-
const destPath =
|
|
2191
|
+
const destPath = resolve3(targetDir, name);
|
|
2037
2192
|
if (!isSafePath(projectDir, destPath)) continue;
|
|
2038
2193
|
let finalPath = destPath;
|
|
2039
2194
|
let finalName = name;
|
|
@@ -2043,13 +2198,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2043
2198
|
const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
|
2044
2199
|
let n = 2;
|
|
2045
2200
|
const MAX_COPY_INDEX = 1e4;
|
|
2046
|
-
while (n < MAX_COPY_INDEX && existsSync3(
|
|
2201
|
+
while (n < MAX_COPY_INDEX && existsSync3(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
|
|
2047
2202
|
if (n >= MAX_COPY_INDEX) {
|
|
2048
2203
|
skipped.push(name);
|
|
2049
2204
|
continue;
|
|
2050
2205
|
}
|
|
2051
2206
|
finalName = `${base} (${n})${ext}`;
|
|
2052
|
-
finalPath =
|
|
2207
|
+
finalPath = resolve3(targetDir, finalName);
|
|
2053
2208
|
}
|
|
2054
2209
|
const buffer = Buffer.from(await value.arrayBuffer());
|
|
2055
2210
|
const validation = validateUploadedMediaBuffer(finalName, buffer);
|
|
@@ -2077,7 +2232,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2077
2232
|
}
|
|
2078
2233
|
return c.json({ error: "not found" }, 404);
|
|
2079
2234
|
}
|
|
2080
|
-
const content =
|
|
2235
|
+
const content = readFileSync4(res.absPath, "utf-8");
|
|
2081
2236
|
return c.json({ filename: res.filePath, content });
|
|
2082
2237
|
});
|
|
2083
2238
|
api.put("/projects/:id/files/*", async (c) => {
|
|
@@ -2129,7 +2284,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2129
2284
|
}
|
|
2130
2285
|
const parsed = await parseMutationBody(c);
|
|
2131
2286
|
if ("error" in parsed) return parsed.error;
|
|
2132
|
-
const originalContent =
|
|
2287
|
+
const originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2133
2288
|
return writeIfChanged(
|
|
2134
2289
|
c,
|
|
2135
2290
|
ctx.project.dir,
|
|
@@ -2150,7 +2305,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2150
2305
|
const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
|
|
2151
2306
|
let originalContent;
|
|
2152
2307
|
try {
|
|
2153
|
-
originalContent =
|
|
2308
|
+
originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2154
2309
|
} catch {
|
|
2155
2310
|
return c.json({ error: "not found" }, 404);
|
|
2156
2311
|
}
|
|
@@ -2190,7 +2345,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2190
2345
|
}
|
|
2191
2346
|
let originalContent;
|
|
2192
2347
|
try {
|
|
2193
|
-
originalContent =
|
|
2348
|
+
originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2194
2349
|
} catch {
|
|
2195
2350
|
return c.json({ error: "not found" }, 404);
|
|
2196
2351
|
}
|
|
@@ -2280,7 +2435,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2280
2435
|
}
|
|
2281
2436
|
let originalContent;
|
|
2282
2437
|
try {
|
|
2283
|
-
originalContent =
|
|
2438
|
+
originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2284
2439
|
} catch {
|
|
2285
2440
|
return c.json({ error: "not found" }, 404);
|
|
2286
2441
|
}
|
|
@@ -2322,7 +2477,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2322
2477
|
if ("error" in parsed) return parsed.error;
|
|
2323
2478
|
let originalContent;
|
|
2324
2479
|
try {
|
|
2325
|
-
originalContent =
|
|
2480
|
+
originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2326
2481
|
} catch {
|
|
2327
2482
|
return c.json({ error: "not found" }, 404);
|
|
2328
2483
|
}
|
|
@@ -2351,7 +2506,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2351
2506
|
if ("error" in parsed) return parsed.error;
|
|
2352
2507
|
let content;
|
|
2353
2508
|
try {
|
|
2354
|
-
content =
|
|
2509
|
+
content = readFileSync4(ctx.absPath, "utf-8");
|
|
2355
2510
|
} catch {
|
|
2356
2511
|
return c.json({ exists: false });
|
|
2357
2512
|
}
|
|
@@ -2394,7 +2549,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2394
2549
|
return c.json({ error: "forbidden" }, 403);
|
|
2395
2550
|
}
|
|
2396
2551
|
ensureDir(destAbs);
|
|
2397
|
-
writeFileSync4(destAbs,
|
|
2552
|
+
writeFileSync4(destAbs, readFileSync4(srcAbs));
|
|
2398
2553
|
return c.json({ ok: true, path: copyPath }, 201);
|
|
2399
2554
|
});
|
|
2400
2555
|
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
@@ -2424,7 +2579,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2424
2579
|
mustExist: true
|
|
2425
2580
|
});
|
|
2426
2581
|
if ("error" in res) return res.error;
|
|
2427
|
-
const html =
|
|
2582
|
+
const html = readFileSync4(res.absPath, "utf-8");
|
|
2428
2583
|
const block = extractGsapScriptBlock(html);
|
|
2429
2584
|
if (!block) {
|
|
2430
2585
|
return c.json({
|
|
@@ -2483,7 +2638,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2483
2638
|
if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
|
|
2484
2639
|
return c.json({ error: "expected and restore contents required" }, 400);
|
|
2485
2640
|
}
|
|
2486
|
-
const current =
|
|
2641
|
+
const current = readFileSync4(res.absPath, "utf-8");
|
|
2487
2642
|
if (current !== body.expected) {
|
|
2488
2643
|
return c.json({ ok: true, restored: false, conflict: true });
|
|
2489
2644
|
}
|
|
@@ -2499,7 +2654,7 @@ import { createHash as createHash2 } from "crypto";
|
|
|
2499
2654
|
import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
|
|
2500
2655
|
|
|
2501
2656
|
// src/helpers/subComposition.ts
|
|
2502
|
-
import { existsSync as existsSync4, readFileSync as
|
|
2657
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
2503
2658
|
import { join as join7 } from "path";
|
|
2504
2659
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
2505
2660
|
import {
|
|
@@ -2608,7 +2763,7 @@ function tagRootCompositionFile(bodyHtml, compPath) {
|
|
|
2608
2763
|
function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
|
|
2609
2764
|
const compFile = join7(projectDir, compPath);
|
|
2610
2765
|
if (!existsSync4(compFile)) return null;
|
|
2611
|
-
const rawComp = rawOverride ??
|
|
2766
|
+
const rawComp = rawOverride ?? readFileSync5(compFile, "utf-8");
|
|
2612
2767
|
let compHeadContent = "";
|
|
2613
2768
|
let rewrittenContent;
|
|
2614
2769
|
let htmlAttrs = "";
|
|
@@ -2641,7 +2796,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
2641
2796
|
const indexPath = join7(projectDir, "index.html");
|
|
2642
2797
|
let headContent = "";
|
|
2643
2798
|
if (existsSync4(indexPath)) {
|
|
2644
|
-
const indexHtml =
|
|
2799
|
+
const indexHtml = readFileSync5(indexPath, "utf-8");
|
|
2645
2800
|
const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
2646
2801
|
headContent = headMatch?.[1] ?? "";
|
|
2647
2802
|
}
|
|
@@ -2674,145 +2829,6 @@ ${rewrittenContent}
|
|
|
2674
2829
|
</html>`;
|
|
2675
2830
|
}
|
|
2676
2831
|
|
|
2677
|
-
// src/helpers/projectSignature.ts
|
|
2678
|
-
import { createHash } from "crypto";
|
|
2679
|
-
import { lstatSync, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
|
|
2680
|
-
import { extname, isAbsolute, relative as relative2, resolve as resolve3 } from "path";
|
|
2681
|
-
var SIGNATURE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2682
|
-
".cjs",
|
|
2683
|
-
".css",
|
|
2684
|
-
".html",
|
|
2685
|
-
".js",
|
|
2686
|
-
".json",
|
|
2687
|
-
".jsx",
|
|
2688
|
-
".mjs",
|
|
2689
|
-
".svg",
|
|
2690
|
-
".ts",
|
|
2691
|
-
".tsx"
|
|
2692
|
-
]);
|
|
2693
|
-
var SIGNATURE_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
2694
|
-
".cache",
|
|
2695
|
-
".git",
|
|
2696
|
-
".hyperframes",
|
|
2697
|
-
".next",
|
|
2698
|
-
".vite",
|
|
2699
|
-
"build",
|
|
2700
|
-
"coverage",
|
|
2701
|
-
"dist",
|
|
2702
|
-
"node_modules",
|
|
2703
|
-
"outputs",
|
|
2704
|
-
"renders"
|
|
2705
|
-
]);
|
|
2706
|
-
var MAX_SIGNATURE_TEXT_BYTES = 2e6;
|
|
2707
|
-
var STUDIO_SIGNATURE_MANIFEST_PATHS = [
|
|
2708
|
-
".hyperframes/studio-manual-edits.json",
|
|
2709
|
-
".hyperframes/studio-motion.json"
|
|
2710
|
-
];
|
|
2711
|
-
var projectSignatureCache = /* @__PURE__ */ new Map();
|
|
2712
|
-
function isPathWithin(parentDir, childPath) {
|
|
2713
|
-
const childRelativePath = relative2(parentDir, childPath);
|
|
2714
|
-
return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute(childRelativePath);
|
|
2715
|
-
}
|
|
2716
|
-
function isTextContentEligible(file, size) {
|
|
2717
|
-
return SIGNATURE_TEXT_EXTENSIONS.has(extname(file).toLowerCase()) && size <= MAX_SIGNATURE_TEXT_BYTES;
|
|
2718
|
-
}
|
|
2719
|
-
function collectProjectSignatureFiles(projectDir, dir, files) {
|
|
2720
|
-
let entries;
|
|
2721
|
-
try {
|
|
2722
|
-
entries = readdirSync4(dir).sort();
|
|
2723
|
-
} catch {
|
|
2724
|
-
return;
|
|
2725
|
-
}
|
|
2726
|
-
for (const entry of entries) {
|
|
2727
|
-
if (SIGNATURE_EXCLUDED_DIRS.has(entry)) continue;
|
|
2728
|
-
const file = resolve3(dir, entry);
|
|
2729
|
-
if (!isPathWithin(projectDir, file)) continue;
|
|
2730
|
-
let stat;
|
|
2731
|
-
try {
|
|
2732
|
-
stat = lstatSync(file);
|
|
2733
|
-
} catch {
|
|
2734
|
-
continue;
|
|
2735
|
-
}
|
|
2736
|
-
if (stat.isSymbolicLink()) continue;
|
|
2737
|
-
if (stat.isDirectory()) {
|
|
2738
|
-
collectProjectSignatureFiles(projectDir, file, files);
|
|
2739
|
-
} else if (stat.isFile()) {
|
|
2740
|
-
files.push({
|
|
2741
|
-
file,
|
|
2742
|
-
mtimeMs: stat.mtimeMs,
|
|
2743
|
-
size: stat.size,
|
|
2744
|
-
textContentEligible: isTextContentEligible(file, stat.size)
|
|
2745
|
-
});
|
|
2746
|
-
}
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
function collectProjectSignatureManifestFiles(projectDir, files) {
|
|
2750
|
-
const seen = new Set(files.map((entry) => entry.file));
|
|
2751
|
-
for (const manifestPath of STUDIO_SIGNATURE_MANIFEST_PATHS) {
|
|
2752
|
-
const file = resolve3(projectDir, manifestPath);
|
|
2753
|
-
if (seen.has(file) || !isPathWithin(projectDir, file)) continue;
|
|
2754
|
-
let stat;
|
|
2755
|
-
try {
|
|
2756
|
-
stat = lstatSync(file);
|
|
2757
|
-
} catch {
|
|
2758
|
-
continue;
|
|
2759
|
-
}
|
|
2760
|
-
if (stat.isSymbolicLink() || !stat.isFile()) continue;
|
|
2761
|
-
files.push({
|
|
2762
|
-
file,
|
|
2763
|
-
mtimeMs: stat.mtimeMs,
|
|
2764
|
-
size: stat.size,
|
|
2765
|
-
textContentEligible: isTextContentEligible(file, stat.size)
|
|
2766
|
-
});
|
|
2767
|
-
seen.add(file);
|
|
2768
|
-
}
|
|
2769
|
-
}
|
|
2770
|
-
function createProjectFingerprint(projectDir, files) {
|
|
2771
|
-
const hash = createHash("sha256");
|
|
2772
|
-
for (const entry of files) {
|
|
2773
|
-
hash.update(relative2(projectDir, entry.file));
|
|
2774
|
-
hash.update("\0");
|
|
2775
|
-
hash.update(String(entry.size));
|
|
2776
|
-
hash.update("\0");
|
|
2777
|
-
hash.update(String(entry.mtimeMs));
|
|
2778
|
-
hash.update("\0");
|
|
2779
|
-
hash.update(entry.textContentEligible ? "text" : "binary");
|
|
2780
|
-
hash.update("\0");
|
|
2781
|
-
}
|
|
2782
|
-
return hash.digest("hex").slice(0, 24);
|
|
2783
|
-
}
|
|
2784
|
-
function createProjectSignature(projectDir) {
|
|
2785
|
-
const normalizedProjectDir = resolve3(projectDir);
|
|
2786
|
-
const files = [];
|
|
2787
|
-
collectProjectSignatureFiles(normalizedProjectDir, normalizedProjectDir, files);
|
|
2788
|
-
collectProjectSignatureManifestFiles(normalizedProjectDir, files);
|
|
2789
|
-
files.sort((a, b) => a.file.localeCompare(b.file));
|
|
2790
|
-
const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
|
|
2791
|
-
const cached = projectSignatureCache.get(normalizedProjectDir);
|
|
2792
|
-
if (cached?.fingerprint === fingerprint) return cached.signature;
|
|
2793
|
-
const hash = createHash("sha256");
|
|
2794
|
-
for (const entry of files) {
|
|
2795
|
-
const relativePath = relative2(normalizedProjectDir, entry.file);
|
|
2796
|
-
hash.update(relativePath);
|
|
2797
|
-
hash.update("\0");
|
|
2798
|
-
hash.update(String(entry.size));
|
|
2799
|
-
hash.update("\0");
|
|
2800
|
-
if (entry.textContentEligible) {
|
|
2801
|
-
try {
|
|
2802
|
-
hash.update(readFileSync5(entry.file));
|
|
2803
|
-
} catch {
|
|
2804
|
-
hash.update(String(entry.mtimeMs));
|
|
2805
|
-
}
|
|
2806
|
-
} else {
|
|
2807
|
-
hash.update(String(entry.mtimeMs));
|
|
2808
|
-
}
|
|
2809
|
-
hash.update("\0");
|
|
2810
|
-
}
|
|
2811
|
-
const signature = hash.digest("hex").slice(0, 24);
|
|
2812
|
-
projectSignatureCache.set(normalizedProjectDir, { fingerprint, signature });
|
|
2813
|
-
return signature;
|
|
2814
|
-
}
|
|
2815
|
-
|
|
2816
2832
|
// src/helpers/studioMotionRenderScript.ts
|
|
2817
2833
|
var STUDIO_MOTION_PATH = ".hyperframes/studio-motion.json";
|
|
2818
2834
|
function hasStudioMotionEntries(manifestContent) {
|
|
@@ -3078,9 +3094,6 @@ var GSAP_CDN_VERSION = "3.15.0";
|
|
|
3078
3094
|
var GSAP_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/gsap.min.js"></script>`;
|
|
3079
3095
|
var GSAP_CUSTOM_EASE_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/CustomEase.min.js"></script>`;
|
|
3080
3096
|
var GSAP_MOTION_PATH_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/MotionPathPlugin.min.js"></script>`;
|
|
3081
|
-
function resolveProjectSignature(adapter, projectDir) {
|
|
3082
|
-
return adapter.getProjectSignature?.(projectDir) ?? createProjectSignature(projectDir);
|
|
3083
|
-
}
|
|
3084
3097
|
function injectProjectSignature(html, signature) {
|
|
3085
3098
|
const tag = `<meta name="${PROJECT_SIGNATURE_META}" content="${signature}">`;
|
|
3086
3099
|
if (html.includes(`name="${PROJECT_SIGNATURE_META}"`)) {
|
|
@@ -3269,12 +3282,12 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3269
3282
|
ETag: etag
|
|
3270
3283
|
});
|
|
3271
3284
|
api.get("/projects/:id/preview", async (c) => {
|
|
3272
|
-
const
|
|
3273
|
-
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;
|
|
3274
3288
|
const vars = previewVariablesFromRequest(c.req.query("variables"));
|
|
3275
3289
|
if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
|
|
3276
3290
|
const previewVariables = vars.values;
|
|
3277
|
-
const signature = resolveProjectSignature(adapter, project.dir);
|
|
3278
3291
|
const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`;
|
|
3279
3292
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3280
3293
|
if (ifNoneMatch === etag) {
|
|
@@ -3334,12 +3347,12 @@ ${runtimeTag}`;
|
|
|
3334
3347
|
return stampFileHfIds(compFile);
|
|
3335
3348
|
}
|
|
3336
3349
|
api.get("/projects/:id/preview/comp/*", async (c) => {
|
|
3337
|
-
const
|
|
3338
|
-
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;
|
|
3339
3353
|
const vars = previewVariablesFromRequest(c.req.query("variables"));
|
|
3340
3354
|
if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
|
|
3341
3355
|
const previewVariables = vars.values;
|
|
3342
|
-
const signature = resolveProjectSignature(adapter, project.dir);
|
|
3343
3356
|
const compPath = decodeURIComponent(
|
|
3344
3357
|
c.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? ""
|
|
3345
3358
|
);
|
|
@@ -3459,7 +3472,7 @@ import { streamSSE } from "hono/streaming";
|
|
|
3459
3472
|
import { existsSync as existsSync6, readFileSync as readFileSync9, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
3460
3473
|
import { join as join10 } from "path";
|
|
3461
3474
|
import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
|
|
3462
|
-
import { parseFps } from "@hyperframes/core";
|
|
3475
|
+
import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
|
|
3463
3476
|
var VALID_RESOLUTIONS = new Set(VALID_CANVAS_RESOLUTIONS);
|
|
3464
3477
|
function registerRenderRoutes(api, adapter) {
|
|
3465
3478
|
const renderJobs = /* @__PURE__ */ new Map();
|
|
@@ -3513,9 +3526,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3513
3526
|
variables = body.variables;
|
|
3514
3527
|
}
|
|
3515
3528
|
const now = /* @__PURE__ */ new Date();
|
|
3516
|
-
const
|
|
3517
|
-
const timePart = now.toTimeString().slice(0, 8).replace(/:/g, "-");
|
|
3518
|
-
const jobId = `${project.id}_${datePart}_${timePart}`;
|
|
3529
|
+
const jobId = `${project.id}_${formatRenderOutputTimestamp(now)}`;
|
|
3519
3530
|
const rendersDir = adapter.rendersDir(project);
|
|
3520
3531
|
if (!existsSync6(rendersDir)) mkdirSync4(rendersDir, { recursive: true });
|
|
3521
3532
|
const ext = FORMAT_EXT[format] ?? ".mp4";
|