@hyperframes/studio-server 0.7.58 → 0.7.60
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.d.ts +11 -1
- package/dist/index.js +370 -216
- 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
|
}
|
|
@@ -133,17 +288,21 @@ function registerStoryboardRoutes(api, adapter) {
|
|
|
133
288
|
// src/routes/files.ts
|
|
134
289
|
import { bodyLimit } from "hono/body-limit";
|
|
135
290
|
import {
|
|
291
|
+
closeSync,
|
|
136
292
|
existsSync as existsSync3,
|
|
137
|
-
|
|
293
|
+
ftruncateSync,
|
|
294
|
+
openSync,
|
|
295
|
+
readFileSync as readFileSync4,
|
|
138
296
|
writeFileSync as writeFileSync4,
|
|
297
|
+
writeSync,
|
|
139
298
|
mkdirSync as mkdirSync3,
|
|
140
299
|
unlinkSync as unlinkSync2,
|
|
141
300
|
rmSync as rmSync2,
|
|
142
301
|
statSync,
|
|
143
302
|
renameSync,
|
|
144
|
-
readdirSync as
|
|
303
|
+
readdirSync as readdirSync4
|
|
145
304
|
} from "fs";
|
|
146
|
-
import { resolve as
|
|
305
|
+
import { resolve as resolve3, dirname, join as join6 } from "path";
|
|
147
306
|
|
|
148
307
|
// src/helpers/mime.ts
|
|
149
308
|
var MIME_TYPES = {
|
|
@@ -188,7 +347,7 @@ function isAudioFile(name) {
|
|
|
188
347
|
// src/helpers/waveform.ts
|
|
189
348
|
import { spawn } from "child_process";
|
|
190
349
|
import { existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
|
|
191
|
-
import { join as join3, resolve } from "path";
|
|
350
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
192
351
|
var SAMPLE_RATE = 4e3;
|
|
193
352
|
var PEAK_COUNT = 4e3;
|
|
194
353
|
var WAVEFORM_CACHE_VERSION = "v2";
|
|
@@ -213,7 +372,7 @@ function computePeaks(floats, count) {
|
|
|
213
372
|
}
|
|
214
373
|
function ffmpegBinary() {
|
|
215
374
|
const configured = process.env.HYPERFRAMES_FFMPEG_PATH?.trim();
|
|
216
|
-
if (configured) return
|
|
375
|
+
if (configured) return resolve2(configured);
|
|
217
376
|
return "ffmpeg";
|
|
218
377
|
}
|
|
219
378
|
function decodeAudioPeaks(audioPath) {
|
|
@@ -322,9 +481,9 @@ function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
|
|
|
322
481
|
}
|
|
323
482
|
|
|
324
483
|
// src/helpers/backupJournal.ts
|
|
325
|
-
import { mkdirSync as mkdirSync2, readdirSync as
|
|
484
|
+
import { mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
326
485
|
import { Buffer as Buffer2 } from "buffer";
|
|
327
|
-
import { join as join5, relative } from "path";
|
|
486
|
+
import { join as join5, relative as relative2 } from "path";
|
|
328
487
|
var DEFAULT_KEEP_PER_FILE = 10;
|
|
329
488
|
function backupKeyForPath(path) {
|
|
330
489
|
return Buffer2.from(path, "utf-8").toString("base64url");
|
|
@@ -334,15 +493,15 @@ function timestampPrefix() {
|
|
|
334
493
|
}
|
|
335
494
|
function backupPathForResponse(projectDir, backupPath) {
|
|
336
495
|
if (!backupPath) return null;
|
|
337
|
-
const rel =
|
|
496
|
+
const rel = relative2(projectDir, backupPath);
|
|
338
497
|
if (!rel || rel.startsWith("..")) return null;
|
|
339
498
|
return rel.split("\\").join("/");
|
|
340
499
|
}
|
|
341
500
|
function snapshotBeforeWrite(projectDir, absPath, options = {}) {
|
|
342
501
|
if (!isSafePath(projectDir, absPath)) return { backupPath: null };
|
|
343
502
|
try {
|
|
344
|
-
const content =
|
|
345
|
-
const relativePath =
|
|
503
|
+
const content = readFileSync3(absPath);
|
|
504
|
+
const relativePath = relative2(projectDir, absPath);
|
|
346
505
|
const backupDir = join5(projectDir, ".hyperframes", "backup");
|
|
347
506
|
mkdirSync2(backupDir, { recursive: true });
|
|
348
507
|
const backupKey = backupKeyForPath(relativePath);
|
|
@@ -363,7 +522,7 @@ function nextBackupPath(backupDir, backupKey) {
|
|
|
363
522
|
let counter = 2;
|
|
364
523
|
while (true) {
|
|
365
524
|
try {
|
|
366
|
-
|
|
525
|
+
readFileSync3(candidate);
|
|
367
526
|
} catch (error) {
|
|
368
527
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
369
528
|
return candidate;
|
|
@@ -378,7 +537,7 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
|
|
|
378
537
|
const keep = Math.max(1, Math.floor(keepPerFile));
|
|
379
538
|
const suffix = `-${backupKey}`;
|
|
380
539
|
const numberedSuffix = new RegExp(`-${backupKey}-\\d+$`);
|
|
381
|
-
const matches =
|
|
540
|
+
const matches = readdirSync3(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join5(backupDir, name)).sort((a, b) => {
|
|
382
541
|
return b.localeCompare(a);
|
|
383
542
|
});
|
|
384
543
|
for (const file of matches.slice(keep)) {
|
|
@@ -389,6 +548,38 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
|
|
|
389
548
|
}
|
|
390
549
|
}
|
|
391
550
|
|
|
551
|
+
// src/helpers/fileVersion.ts
|
|
552
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
553
|
+
var RECEIPT_TTL_MS = 1e4;
|
|
554
|
+
var receipts = /* @__PURE__ */ new Map();
|
|
555
|
+
function fileContentVersion(content) {
|
|
556
|
+
return `"sha256:${createHash2("sha256").update(content, "utf8").digest("hex")}"`;
|
|
557
|
+
}
|
|
558
|
+
function createWriteToken(requestToken) {
|
|
559
|
+
const token = requestToken?.trim();
|
|
560
|
+
return token && token.length <= 200 ? token : randomUUID();
|
|
561
|
+
}
|
|
562
|
+
function recordFileWriteReceipt(absPath, receipt) {
|
|
563
|
+
const now = Date.now();
|
|
564
|
+
const current = (receipts.get(absPath) ?? []).filter(
|
|
565
|
+
(entry) => now - entry.recordedAt < RECEIPT_TTL_MS
|
|
566
|
+
);
|
|
567
|
+
current.push({ ...receipt, recordedAt: now });
|
|
568
|
+
receipts.set(absPath, current);
|
|
569
|
+
}
|
|
570
|
+
function consumeFileWriteReceipt(absPath) {
|
|
571
|
+
const now = Date.now();
|
|
572
|
+
const current = (receipts.get(absPath) ?? []).filter(
|
|
573
|
+
(entry) => now - entry.recordedAt < RECEIPT_TTL_MS
|
|
574
|
+
);
|
|
575
|
+
const receipt = current.shift() ?? null;
|
|
576
|
+
if (current.length > 0) receipts.set(absPath, current);
|
|
577
|
+
else receipts.delete(absPath);
|
|
578
|
+
if (!receipt) return null;
|
|
579
|
+
const { path, version, writeToken } = receipt;
|
|
580
|
+
return { path, version, writeToken };
|
|
581
|
+
}
|
|
582
|
+
|
|
392
583
|
// src/helpers/finiteMutation.ts
|
|
393
584
|
function findUnsafeMutationValues(value, path = "body", options = {}) {
|
|
394
585
|
if (value === null) {
|
|
@@ -931,7 +1122,7 @@ function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSyn
|
|
|
931
1122
|
resolvedPaths.add(absPath);
|
|
932
1123
|
let before;
|
|
933
1124
|
try {
|
|
934
|
-
before =
|
|
1125
|
+
before = readFileSync4(absPath, "utf-8");
|
|
935
1126
|
} catch {
|
|
936
1127
|
return { error: "not-found", sourceFile: batch.sourceFile };
|
|
937
1128
|
}
|
|
@@ -1053,7 +1244,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1053
1244
|
const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
|
|
1054
1245
|
let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
|
|
1055
1246
|
let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
|
|
1056
|
-
while (existsSync3(
|
|
1247
|
+
while (existsSync3(resolve3(projectDir, candidate))) {
|
|
1057
1248
|
num++;
|
|
1058
1249
|
candidate = `${cleanBase} (copy ${num})${ext}`;
|
|
1059
1250
|
}
|
|
@@ -1061,7 +1252,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1061
1252
|
}
|
|
1062
1253
|
function walkFiles(dir, filter) {
|
|
1063
1254
|
const results = [];
|
|
1064
|
-
for (const entry of
|
|
1255
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
1065
1256
|
const full = join6(dir, entry.name);
|
|
1066
1257
|
if (entry.isDirectory()) {
|
|
1067
1258
|
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders")
|
|
@@ -1080,7 +1271,7 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
1080
1271
|
);
|
|
1081
1272
|
let updatedCount = 0;
|
|
1082
1273
|
for (const file of textFiles) {
|
|
1083
|
-
const content =
|
|
1274
|
+
const content = readFileSync4(file, "utf-8");
|
|
1084
1275
|
if (!content.includes(oldPath)) continue;
|
|
1085
1276
|
const updated = content.split(oldPath).join(newPath);
|
|
1086
1277
|
if (updated !== content) {
|
|
@@ -1337,7 +1528,7 @@ function validateGsapMutationRequest(c, body) {
|
|
|
1337
1528
|
return null;
|
|
1338
1529
|
}
|
|
1339
1530
|
async function prepareGsapMutationScript(c, res, firstMutation) {
|
|
1340
|
-
const beforeHtml =
|
|
1531
|
+
const beforeHtml = readFileSync4(res.absPath, "utf-8");
|
|
1341
1532
|
let html = beforeHtml;
|
|
1342
1533
|
let block = extractGsapScriptBlock(html);
|
|
1343
1534
|
if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
|
|
@@ -1397,7 +1588,7 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1397
1588
|
const changed = block.scriptText !== initialScript;
|
|
1398
1589
|
const newHtml = changed ? block.replaceScript(block.scriptText) : html;
|
|
1399
1590
|
let backupPath = null;
|
|
1400
|
-
if (
|
|
1591
|
+
if (readFileSync4(res.absPath, "utf-8") !== beforeHtml) {
|
|
1401
1592
|
return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
|
|
1402
1593
|
}
|
|
1403
1594
|
if (changed) {
|
|
@@ -1415,9 +1606,11 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1415
1606
|
after: newHtml,
|
|
1416
1607
|
scriptText: block.scriptText,
|
|
1417
1608
|
path: res.filePath,
|
|
1609
|
+
version: fileContentVersion(newHtml),
|
|
1418
1610
|
backupPath
|
|
1419
1611
|
};
|
|
1420
1612
|
if (skippedSelectors.size > 0) responsePayload.skippedSelectors = [...skippedSelectors];
|
|
1613
|
+
c.header("ETag", responsePayload.version);
|
|
1421
1614
|
return c.json(responsePayload);
|
|
1422
1615
|
}
|
|
1423
1616
|
function executeGsapMutationAcorn(body, block, respond) {
|
|
@@ -2033,7 +2226,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2033
2226
|
skipped.push(name);
|
|
2034
2227
|
continue;
|
|
2035
2228
|
}
|
|
2036
|
-
const destPath =
|
|
2229
|
+
const destPath = resolve3(targetDir, name);
|
|
2037
2230
|
if (!isSafePath(projectDir, destPath)) continue;
|
|
2038
2231
|
let finalPath = destPath;
|
|
2039
2232
|
let finalName = name;
|
|
@@ -2043,13 +2236,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2043
2236
|
const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
|
2044
2237
|
let n = 2;
|
|
2045
2238
|
const MAX_COPY_INDEX = 1e4;
|
|
2046
|
-
while (n < MAX_COPY_INDEX && existsSync3(
|
|
2239
|
+
while (n < MAX_COPY_INDEX && existsSync3(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
|
|
2047
2240
|
if (n >= MAX_COPY_INDEX) {
|
|
2048
2241
|
skipped.push(name);
|
|
2049
2242
|
continue;
|
|
2050
2243
|
}
|
|
2051
2244
|
finalName = `${base} (${n})${ext}`;
|
|
2052
|
-
finalPath =
|
|
2245
|
+
finalPath = resolve3(targetDir, finalName);
|
|
2053
2246
|
}
|
|
2054
2247
|
const buffer = Buffer.from(await value.arrayBuffer());
|
|
2055
2248
|
const validation = validateUploadedMediaBuffer(finalName, buffer);
|
|
@@ -2077,20 +2270,112 @@ function registerFileRoutes(api, adapter) {
|
|
|
2077
2270
|
}
|
|
2078
2271
|
return c.json({ error: "not found" }, 404);
|
|
2079
2272
|
}
|
|
2080
|
-
const content =
|
|
2081
|
-
|
|
2273
|
+
const content = readFileSync4(res.absPath, "utf-8");
|
|
2274
|
+
const version = fileContentVersion(content);
|
|
2275
|
+
c.header("ETag", version);
|
|
2276
|
+
return c.json({ filename: res.filePath, content, version });
|
|
2082
2277
|
});
|
|
2083
2278
|
api.put("/projects/:id/files/*", async (c) => {
|
|
2084
2279
|
const res = await resolveProjectFile(c, adapter);
|
|
2085
2280
|
if ("error" in res) return res.error;
|
|
2086
|
-
ensureDir(res.absPath);
|
|
2087
2281
|
const body = await c.req.text();
|
|
2088
|
-
const
|
|
2089
|
-
|
|
2090
|
-
|
|
2282
|
+
const expectedVersion = c.req.header("If-Match")?.trim() ?? null;
|
|
2283
|
+
const createOnly = c.req.header("If-None-Match")?.trim() === "*";
|
|
2284
|
+
if (expectedVersion === null && !createOnly) {
|
|
2285
|
+
let currentContent = null;
|
|
2286
|
+
try {
|
|
2287
|
+
currentContent = readFileSync4(res.absPath, "utf-8");
|
|
2288
|
+
} catch (error) {
|
|
2289
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2290
|
+
throw error;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
return c.json(
|
|
2294
|
+
{
|
|
2295
|
+
error: "precondition required",
|
|
2296
|
+
path: res.filePath,
|
|
2297
|
+
currentVersion: currentContent === null ? null : fileContentVersion(currentContent),
|
|
2298
|
+
currentContent
|
|
2299
|
+
},
|
|
2300
|
+
428
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
let backup = { backupPath: null };
|
|
2304
|
+
if (createOnly) {
|
|
2305
|
+
ensureDir(res.absPath);
|
|
2306
|
+
let fd;
|
|
2307
|
+
try {
|
|
2308
|
+
fd = openSync(res.absPath, "wx");
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
2311
|
+
throw error;
|
|
2312
|
+
}
|
|
2313
|
+
const currentContent = readFileSync4(res.absPath, "utf-8");
|
|
2314
|
+
return c.json(
|
|
2315
|
+
{
|
|
2316
|
+
error: "file conflict",
|
|
2317
|
+
path: res.filePath,
|
|
2318
|
+
currentVersion: fileContentVersion(currentContent),
|
|
2319
|
+
currentContent
|
|
2320
|
+
},
|
|
2321
|
+
409
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
try {
|
|
2325
|
+
writeSync(fd, body, 0, "utf-8");
|
|
2326
|
+
} finally {
|
|
2327
|
+
closeSync(fd);
|
|
2328
|
+
}
|
|
2329
|
+
} else {
|
|
2330
|
+
let fd;
|
|
2331
|
+
try {
|
|
2332
|
+
fd = openSync(res.absPath, "r+");
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2335
|
+
throw error;
|
|
2336
|
+
}
|
|
2337
|
+
return c.json(
|
|
2338
|
+
{
|
|
2339
|
+
error: "file conflict",
|
|
2340
|
+
path: res.filePath,
|
|
2341
|
+
currentVersion: null,
|
|
2342
|
+
currentContent: null
|
|
2343
|
+
},
|
|
2344
|
+
409
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
try {
|
|
2348
|
+
const currentContent = readFileSync4(fd, "utf-8");
|
|
2349
|
+
const currentVersion = fileContentVersion(currentContent);
|
|
2350
|
+
if (expectedVersion !== currentVersion) {
|
|
2351
|
+
return c.json(
|
|
2352
|
+
{
|
|
2353
|
+
error: "file conflict",
|
|
2354
|
+
path: res.filePath,
|
|
2355
|
+
currentVersion,
|
|
2356
|
+
currentContent
|
|
2357
|
+
},
|
|
2358
|
+
409
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2361
|
+
backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
2362
|
+
if (backup.error)
|
|
2363
|
+
console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
|
|
2364
|
+
ftruncateSync(fd, 0);
|
|
2365
|
+
writeSync(fd, body, 0, "utf-8");
|
|
2366
|
+
} finally {
|
|
2367
|
+
closeSync(fd);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
const version = fileContentVersion(body);
|
|
2371
|
+
const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
|
|
2372
|
+
recordFileWriteReceipt(res.absPath, { path: res.filePath, version, writeToken });
|
|
2373
|
+
c.header("ETag", version);
|
|
2091
2374
|
return c.json({
|
|
2092
2375
|
ok: true,
|
|
2093
2376
|
path: res.filePath,
|
|
2377
|
+
version,
|
|
2378
|
+
writeToken,
|
|
2094
2379
|
backupPath: backupPathForResponse(res.project.dir, backup.backupPath)
|
|
2095
2380
|
});
|
|
2096
2381
|
});
|
|
@@ -2129,7 +2414,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2129
2414
|
}
|
|
2130
2415
|
const parsed = await parseMutationBody(c);
|
|
2131
2416
|
if ("error" in parsed) return parsed.error;
|
|
2132
|
-
const originalContent =
|
|
2417
|
+
const originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2133
2418
|
return writeIfChanged(
|
|
2134
2419
|
c,
|
|
2135
2420
|
ctx.project.dir,
|
|
@@ -2150,7 +2435,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2150
2435
|
const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
|
|
2151
2436
|
let originalContent;
|
|
2152
2437
|
try {
|
|
2153
|
-
originalContent =
|
|
2438
|
+
originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2154
2439
|
} catch {
|
|
2155
2440
|
return c.json({ error: "not found" }, 404);
|
|
2156
2441
|
}
|
|
@@ -2162,17 +2447,28 @@ function registerFileRoutes(api, adapter) {
|
|
|
2162
2447
|
fallbackTiming
|
|
2163
2448
|
);
|
|
2164
2449
|
if (!result.matched) {
|
|
2165
|
-
|
|
2450
|
+
const version2 = fileContentVersion(originalContent);
|
|
2451
|
+
c.header("ETag", version2);
|
|
2452
|
+
return c.json({
|
|
2453
|
+
ok: false,
|
|
2454
|
+
changed: false,
|
|
2455
|
+
content: originalContent,
|
|
2456
|
+
path: ctx.filePath,
|
|
2457
|
+
version: version2
|
|
2458
|
+
});
|
|
2166
2459
|
}
|
|
2167
2460
|
const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
|
|
2168
2461
|
if (backup.error) console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
|
|
2169
2462
|
writeFileSync4(ctx.absPath, result.html, "utf-8");
|
|
2463
|
+
const version = fileContentVersion(result.html);
|
|
2464
|
+
c.header("ETag", version);
|
|
2170
2465
|
return c.json({
|
|
2171
2466
|
ok: true,
|
|
2172
2467
|
changed: true,
|
|
2173
2468
|
content: result.html,
|
|
2174
2469
|
newId: result.newId,
|
|
2175
2470
|
path: ctx.filePath,
|
|
2471
|
+
version,
|
|
2176
2472
|
backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
|
|
2177
2473
|
});
|
|
2178
2474
|
});
|
|
@@ -2190,7 +2486,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2190
2486
|
}
|
|
2191
2487
|
let originalContent;
|
|
2192
2488
|
try {
|
|
2193
|
-
originalContent =
|
|
2489
|
+
originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2194
2490
|
} catch {
|
|
2195
2491
|
return c.json({ error: "not found" }, 404);
|
|
2196
2492
|
}
|
|
@@ -2280,7 +2576,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2280
2576
|
}
|
|
2281
2577
|
let originalContent;
|
|
2282
2578
|
try {
|
|
2283
|
-
originalContent =
|
|
2579
|
+
originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2284
2580
|
} catch {
|
|
2285
2581
|
return c.json({ error: "not found" }, 404);
|
|
2286
2582
|
}
|
|
@@ -2322,7 +2618,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2322
2618
|
if ("error" in parsed) return parsed.error;
|
|
2323
2619
|
let originalContent;
|
|
2324
2620
|
try {
|
|
2325
|
-
originalContent =
|
|
2621
|
+
originalContent = readFileSync4(ctx.absPath, "utf-8");
|
|
2326
2622
|
} catch {
|
|
2327
2623
|
return c.json({ error: "not found" }, 404);
|
|
2328
2624
|
}
|
|
@@ -2351,7 +2647,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2351
2647
|
if ("error" in parsed) return parsed.error;
|
|
2352
2648
|
let content;
|
|
2353
2649
|
try {
|
|
2354
|
-
content =
|
|
2650
|
+
content = readFileSync4(ctx.absPath, "utf-8");
|
|
2355
2651
|
} catch {
|
|
2356
2652
|
return c.json({ exists: false });
|
|
2357
2653
|
}
|
|
@@ -2394,7 +2690,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2394
2690
|
return c.json({ error: "forbidden" }, 403);
|
|
2395
2691
|
}
|
|
2396
2692
|
ensureDir(destAbs);
|
|
2397
|
-
writeFileSync4(destAbs,
|
|
2693
|
+
writeFileSync4(destAbs, readFileSync4(srcAbs));
|
|
2398
2694
|
return c.json({ ok: true, path: copyPath }, 201);
|
|
2399
2695
|
});
|
|
2400
2696
|
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
@@ -2424,7 +2720,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2424
2720
|
mustExist: true
|
|
2425
2721
|
});
|
|
2426
2722
|
if ("error" in res) return res.error;
|
|
2427
|
-
const html =
|
|
2723
|
+
const html = readFileSync4(res.absPath, "utf-8");
|
|
2428
2724
|
const block = extractGsapScriptBlock(html);
|
|
2429
2725
|
if (!block) {
|
|
2430
2726
|
return c.json({
|
|
@@ -2483,7 +2779,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2483
2779
|
if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
|
|
2484
2780
|
return c.json({ error: "expected and restore contents required" }, 400);
|
|
2485
2781
|
}
|
|
2486
|
-
const current =
|
|
2782
|
+
const current = readFileSync4(res.absPath, "utf-8");
|
|
2487
2783
|
if (current !== body.expected) {
|
|
2488
2784
|
return c.json({ ok: true, restored: false, conflict: true });
|
|
2489
2785
|
}
|
|
@@ -2495,11 +2791,11 @@ function registerFileRoutes(api, adapter) {
|
|
|
2495
2791
|
// src/routes/preview.ts
|
|
2496
2792
|
import { existsSync as existsSync5, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
2497
2793
|
import { join as join8 } from "path";
|
|
2498
|
-
import { createHash as
|
|
2794
|
+
import { createHash as createHash3 } from "crypto";
|
|
2499
2795
|
import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
|
|
2500
2796
|
|
|
2501
2797
|
// src/helpers/subComposition.ts
|
|
2502
|
-
import { existsSync as existsSync4, readFileSync as
|
|
2798
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
2503
2799
|
import { join as join7 } from "path";
|
|
2504
2800
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
2505
2801
|
import {
|
|
@@ -2608,7 +2904,7 @@ function tagRootCompositionFile(bodyHtml, compPath) {
|
|
|
2608
2904
|
function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
|
|
2609
2905
|
const compFile = join7(projectDir, compPath);
|
|
2610
2906
|
if (!existsSync4(compFile)) return null;
|
|
2611
|
-
const rawComp = rawOverride ??
|
|
2907
|
+
const rawComp = rawOverride ?? readFileSync5(compFile, "utf-8");
|
|
2612
2908
|
let compHeadContent = "";
|
|
2613
2909
|
let rewrittenContent;
|
|
2614
2910
|
let htmlAttrs = "";
|
|
@@ -2641,7 +2937,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
2641
2937
|
const indexPath = join7(projectDir, "index.html");
|
|
2642
2938
|
let headContent = "";
|
|
2643
2939
|
if (existsSync4(indexPath)) {
|
|
2644
|
-
const indexHtml =
|
|
2940
|
+
const indexHtml = readFileSync5(indexPath, "utf-8");
|
|
2645
2941
|
const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
2646
2942
|
headContent = headMatch?.[1] ?? "";
|
|
2647
2943
|
}
|
|
@@ -2674,145 +2970,6 @@ ${rewrittenContent}
|
|
|
2674
2970
|
</html>`;
|
|
2675
2971
|
}
|
|
2676
2972
|
|
|
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
2973
|
// src/helpers/studioMotionRenderScript.ts
|
|
2817
2974
|
var STUDIO_MOTION_PATH = ".hyperframes/studio-motion.json";
|
|
2818
2975
|
function hasStudioMotionEntries(manifestContent) {
|
|
@@ -3006,14 +3163,14 @@ import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
|
|
|
3006
3163
|
// src/helpers/hfIdPersist.ts
|
|
3007
3164
|
import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
|
|
3008
3165
|
import {
|
|
3009
|
-
closeSync,
|
|
3166
|
+
closeSync as closeSync2,
|
|
3010
3167
|
constants,
|
|
3011
3168
|
fstatSync,
|
|
3012
|
-
ftruncateSync,
|
|
3013
|
-
openSync,
|
|
3169
|
+
ftruncateSync as ftruncateSync2,
|
|
3170
|
+
openSync as openSync2,
|
|
3014
3171
|
readFileSync as readFileSync6,
|
|
3015
3172
|
writeFileSync as writeFileSync5,
|
|
3016
|
-
writeSync
|
|
3173
|
+
writeSync as writeSync2
|
|
3017
3174
|
} from "fs";
|
|
3018
3175
|
function persistHfIdsIfNeeded(filePath, html) {
|
|
3019
3176
|
const normalized = ensureHfIds2(html);
|
|
@@ -3034,7 +3191,7 @@ function persistHfIdsIfNeeded(filePath, html) {
|
|
|
3034
3191
|
function openNoFollow(filePath, flags) {
|
|
3035
3192
|
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
3036
3193
|
try {
|
|
3037
|
-
return
|
|
3194
|
+
return openSync2(filePath, flags | noFollow);
|
|
3038
3195
|
} catch {
|
|
3039
3196
|
return null;
|
|
3040
3197
|
}
|
|
@@ -3054,15 +3211,15 @@ function stampFileHfIds(filePath) {
|
|
|
3054
3211
|
const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3055
3212
|
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3056
3213
|
if (writable && idsAfter > idsBefore) {
|
|
3057
|
-
|
|
3058
|
-
|
|
3214
|
+
ftruncateSync2(fd, 0);
|
|
3215
|
+
writeSync2(fd, normalized, 0, "utf-8");
|
|
3059
3216
|
}
|
|
3060
3217
|
return normalized;
|
|
3061
3218
|
} catch (err) {
|
|
3062
3219
|
console.warn("[hyperframes] stampFileHfIds: failed to stamp ids:", err);
|
|
3063
3220
|
return null;
|
|
3064
3221
|
} finally {
|
|
3065
|
-
|
|
3222
|
+
closeSync2(fd);
|
|
3066
3223
|
}
|
|
3067
3224
|
}
|
|
3068
3225
|
|
|
@@ -3078,9 +3235,6 @@ var GSAP_CDN_VERSION = "3.15.0";
|
|
|
3078
3235
|
var GSAP_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/gsap.min.js"></script>`;
|
|
3079
3236
|
var GSAP_CUSTOM_EASE_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/CustomEase.min.js"></script>`;
|
|
3080
3237
|
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
3238
|
function injectProjectSignature(html, signature) {
|
|
3085
3239
|
const tag = `<meta name="${PROJECT_SIGNATURE_META}" content="${signature}">`;
|
|
3086
3240
|
if (html.includes(`name="${PROJECT_SIGNATURE_META}"`)) {
|
|
@@ -3221,7 +3375,7 @@ function parsePreviewVariablesParam(raw) {
|
|
|
3221
3375
|
}
|
|
3222
3376
|
function variablesEtagSalt(raw) {
|
|
3223
3377
|
if (!raw) return "";
|
|
3224
|
-
return `:vars:${
|
|
3378
|
+
return `:vars:${createHash3("sha1").update(raw).digest("hex").slice(0, 12)}`;
|
|
3225
3379
|
}
|
|
3226
3380
|
function previewVariablesFromRequest(rawVariables) {
|
|
3227
3381
|
const parse = parsePreviewVariablesParam(rawVariables);
|
|
@@ -3269,12 +3423,12 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3269
3423
|
ETag: etag
|
|
3270
3424
|
});
|
|
3271
3425
|
api.get("/projects/:id/preview", async (c) => {
|
|
3272
|
-
const
|
|
3273
|
-
if (!
|
|
3426
|
+
const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
|
|
3427
|
+
if (!resolved) return c.json({ error: "not found" }, 404);
|
|
3428
|
+
const { project, signature } = resolved;
|
|
3274
3429
|
const vars = previewVariablesFromRequest(c.req.query("variables"));
|
|
3275
3430
|
if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
|
|
3276
3431
|
const previewVariables = vars.values;
|
|
3277
|
-
const signature = resolveProjectSignature(adapter, project.dir);
|
|
3278
3432
|
const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`;
|
|
3279
3433
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3280
3434
|
if (ifNoneMatch === etag) {
|
|
@@ -3334,12 +3488,12 @@ ${runtimeTag}`;
|
|
|
3334
3488
|
return stampFileHfIds(compFile);
|
|
3335
3489
|
}
|
|
3336
3490
|
api.get("/projects/:id/preview/comp/*", async (c) => {
|
|
3337
|
-
const
|
|
3338
|
-
if (!
|
|
3491
|
+
const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
|
|
3492
|
+
if (!resolved) return c.json({ error: "not found" }, 404);
|
|
3493
|
+
const { project, signature } = resolved;
|
|
3339
3494
|
const vars = previewVariablesFromRequest(c.req.query("variables"));
|
|
3340
3495
|
if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
|
|
3341
3496
|
const previewVariables = vars.values;
|
|
3342
|
-
const signature = resolveProjectSignature(adapter, project.dir);
|
|
3343
3497
|
const compPath = decodeURIComponent(
|
|
3344
3498
|
c.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? ""
|
|
3345
3499
|
);
|
|
@@ -3459,7 +3613,7 @@ import { streamSSE } from "hono/streaming";
|
|
|
3459
3613
|
import { existsSync as existsSync6, readFileSync as readFileSync9, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
3460
3614
|
import { join as join10 } from "path";
|
|
3461
3615
|
import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
|
|
3462
|
-
import { parseFps } from "@hyperframes/core";
|
|
3616
|
+
import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
|
|
3463
3617
|
var VALID_RESOLUTIONS = new Set(VALID_CANVAS_RESOLUTIONS);
|
|
3464
3618
|
function registerRenderRoutes(api, adapter) {
|
|
3465
3619
|
const renderJobs = /* @__PURE__ */ new Map();
|
|
@@ -3513,9 +3667,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3513
3667
|
variables = body.variables;
|
|
3514
3668
|
}
|
|
3515
3669
|
const now = /* @__PURE__ */ new Date();
|
|
3516
|
-
const
|
|
3517
|
-
const timePart = now.toTimeString().slice(0, 8).replace(/:/g, "-");
|
|
3518
|
-
const jobId = `${project.id}_${datePart}_${timePart}`;
|
|
3670
|
+
const jobId = `${project.id}_${formatRenderOutputTimestamp(now)}`;
|
|
3519
3671
|
const rendersDir = adapter.rendersDir(project);
|
|
3520
3672
|
if (!existsSync6(rendersDir)) mkdirSync4(rendersDir, { recursive: true });
|
|
3521
3673
|
const ext = FORMAT_EXT[format] ?? ".mp4";
|
|
@@ -3695,7 +3847,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3695
3847
|
// src/routes/thumbnail.ts
|
|
3696
3848
|
import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
|
|
3697
3849
|
import { join as join11 } from "path";
|
|
3698
|
-
import { createHash as
|
|
3850
|
+
import { createHash as createHash4 } from "crypto";
|
|
3699
3851
|
|
|
3700
3852
|
// src/helpers/manualEditsRenderScript.ts
|
|
3701
3853
|
var STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json";
|
|
@@ -4307,7 +4459,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4307
4459
|
const htmlFile = join11(project.dir, compPath);
|
|
4308
4460
|
if (existsSync7(htmlFile)) {
|
|
4309
4461
|
const html = readFileSync10(htmlFile, "utf-8");
|
|
4310
|
-
sourceKey = `_${
|
|
4462
|
+
sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
|
|
4311
4463
|
sourceMtime = Math.round(statSync4(htmlFile).mtimeMs);
|
|
4312
4464
|
if (!vpWidth) {
|
|
4313
4465
|
const wMatch = html.match(/data-width=["'](\d+)["']/);
|
|
@@ -4320,14 +4472,14 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4320
4472
|
let manualEditsKey = "";
|
|
4321
4473
|
if (existsSync7(manualEditsFile)) {
|
|
4322
4474
|
const manualEditsContent = readFileSync10(manualEditsFile, "utf-8");
|
|
4323
|
-
manualEditsKey = `_${
|
|
4475
|
+
manualEditsKey = `_${createHash4("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
|
|
4324
4476
|
sourceMtime = Math.max(sourceMtime, Math.round(statSync4(manualEditsFile).mtimeMs));
|
|
4325
4477
|
}
|
|
4326
4478
|
const motionFile = join11(project.dir, STUDIO_MOTION_PATH);
|
|
4327
4479
|
let motionKey = "";
|
|
4328
4480
|
if (existsSync7(motionFile)) {
|
|
4329
4481
|
const motionContent = readFileSync10(motionFile, "utf-8");
|
|
4330
|
-
motionKey = `_${
|
|
4482
|
+
motionKey = `_${createHash4("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
|
|
4331
4483
|
sourceMtime = Math.max(sourceMtime, Math.round(statSync4(motionFile).mtimeMs));
|
|
4332
4484
|
}
|
|
4333
4485
|
const previewUrl = compPath === "index.html" ? `http://${c.req.header("host")}/api/projects/${project.id}/preview` : `http://${c.req.header("host")}/api/projects/${project.id}/preview/comp/${compPath}`;
|
|
@@ -4408,7 +4560,7 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4408
4560
|
}
|
|
4409
4561
|
|
|
4410
4562
|
// src/routes/fonts.ts
|
|
4411
|
-
import { closeSync as
|
|
4563
|
+
import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, openSync as openSync3, readSync } from "fs";
|
|
4412
4564
|
import {
|
|
4413
4565
|
collectFontFileEntries,
|
|
4414
4566
|
fontDirectories,
|
|
@@ -4515,7 +4667,7 @@ function registerFontRoutes(api) {
|
|
|
4515
4667
|
if (!located) return c.json({ error: "font not found" }, 404);
|
|
4516
4668
|
let fd;
|
|
4517
4669
|
try {
|
|
4518
|
-
fd =
|
|
4670
|
+
fd = openSync3(located.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
4519
4671
|
} catch {
|
|
4520
4672
|
return c.json({ error: "font file not accessible" }, 404);
|
|
4521
4673
|
}
|
|
@@ -4537,7 +4689,7 @@ function registerFontRoutes(api) {
|
|
|
4537
4689
|
} catch {
|
|
4538
4690
|
return c.json({ error: "failed to read font file" }, 500);
|
|
4539
4691
|
} finally {
|
|
4540
|
-
|
|
4692
|
+
closeSync3(fd);
|
|
4541
4693
|
}
|
|
4542
4694
|
});
|
|
4543
4695
|
}
|
|
@@ -5071,12 +5223,14 @@ export {
|
|
|
5071
5223
|
STUDIO_MANUAL_EDITS_PATH,
|
|
5072
5224
|
STUDIO_MOTION_PATH,
|
|
5073
5225
|
buildSubCompositionHtml,
|
|
5226
|
+
consumeFileWriteReceipt,
|
|
5074
5227
|
createBackgroundRemovalJob,
|
|
5075
5228
|
createProjectSignature,
|
|
5076
5229
|
createStudioApi,
|
|
5077
5230
|
createStudioManualEditsRenderBodyScript,
|
|
5078
5231
|
createStudioMotionRenderBodyScript,
|
|
5079
5232
|
createStudioPositionSeekReapplyScript,
|
|
5233
|
+
fileContentVersion,
|
|
5080
5234
|
getElementScreenshotClip,
|
|
5081
5235
|
getMimeType,
|
|
5082
5236
|
isSafePath,
|