@hyperframes/studio-server 0.8.79 → 0.8.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-X3T65KC2.js → chunk-FTDTO3QP.js} +3 -3
- package/dist/{chunk-45POWYFS.js → chunk-G6IIFKXY.js} +7 -14
- package/dist/{chunk-45POWYFS.js.map → chunk-G6IIFKXY.js.map} +1 -1
- package/dist/{chunk-QYDRVMJ6.js → chunk-P5EJPIH3.js} +5 -3
- package/dist/chunk-P5EJPIH3.js.map +1 -0
- package/dist/{chunk-4MNTZVVZ.js → chunk-TJQ76IEA.js} +81 -11
- package/dist/chunk-TJQ76IEA.js.map +1 -0
- package/dist/helpers/mediaCodecMap.d.ts +7 -7
- package/dist/helpers/mediaCodecMap.js +1 -1
- package/dist/helpers/mediaProxyPreview.d.ts +1 -1
- package/dist/helpers/mediaProxyPreview.js +3 -3
- package/dist/helpers/proxyTranscoder.js +2 -2
- package/dist/helpers/sourceMutation.js +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +522 -346
- package/dist/index.js.map +1 -1
- package/dist/{mediaProxyPreview-rAYw-Jeh.d.ts → mediaProxyPreview-BOp3Wa9v.d.ts} +5 -4
- package/package.json +4 -3
- package/dist/chunk-4MNTZVVZ.js.map +0 -1
- package/dist/chunk-QYDRVMJ6.js.map +0 -1
- /package/dist/{chunk-X3T65KC2.js.map → chunk-FTDTO3QP.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
splitElementInHtml,
|
|
14
14
|
unwrapElementsFromHtml,
|
|
15
15
|
wrapElementsInHtml
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-P5EJPIH3.js";
|
|
17
17
|
import {
|
|
18
18
|
getElementScreenshotClip
|
|
19
19
|
} from "./chunk-W2SBTCO2.js";
|
|
@@ -22,21 +22,28 @@ import {
|
|
|
22
22
|
isAutoProxyEnabled,
|
|
23
23
|
proxyEtagSalt,
|
|
24
24
|
resolvePreviewMediaCodecProbeCache
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-FTDTO3QP.js";
|
|
26
26
|
import {
|
|
27
27
|
ProxyCapacityError,
|
|
28
28
|
ProxyTranscodeError,
|
|
29
29
|
resolveProxy
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-G6IIFKXY.js";
|
|
31
31
|
import {
|
|
32
32
|
PROXY_VARIANT_CONFIG,
|
|
33
33
|
decideMediaProxyEligibility,
|
|
34
|
+
isInHiddenOrVendorDir,
|
|
34
35
|
isProxyVariantRequest,
|
|
36
|
+
isSafePath,
|
|
37
|
+
pinWithinProject,
|
|
35
38
|
probeAssetCodec,
|
|
36
39
|
probeMediaMetadata,
|
|
40
|
+
realFilePath,
|
|
41
|
+
realpath,
|
|
37
42
|
recordProxyRequest,
|
|
38
|
-
resolveProxyVariantRequest
|
|
39
|
-
|
|
43
|
+
resolveProxyVariantRequest,
|
|
44
|
+
resolveWithinProject,
|
|
45
|
+
walkDir
|
|
46
|
+
} from "./chunk-TJQ76IEA.js";
|
|
40
47
|
import {
|
|
41
48
|
STUDIO_MANUAL_EDITS_PATH,
|
|
42
49
|
createStudioManualEditsRenderBodyScript,
|
|
@@ -52,64 +59,7 @@ import { Hono } from "hono";
|
|
|
52
59
|
|
|
53
60
|
// src/routes/projects.ts
|
|
54
61
|
import { readFile } from "fs/promises";
|
|
55
|
-
import { join as
|
|
56
|
-
|
|
57
|
-
// src/helpers/safePath.ts
|
|
58
|
-
import { basename, dirname, isAbsolute, join, relative, sep } from "path";
|
|
59
|
-
import { readdirSync, realpathSync } from "fs";
|
|
60
|
-
import { resolveWithinProject } from "@hyperframes/core";
|
|
61
|
-
import { isSafePath, resolveWithinProject as resolveWithinProject2 } from "@hyperframes/core";
|
|
62
|
-
function realFilePath(filePath) {
|
|
63
|
-
try {
|
|
64
|
-
return realpathSync(filePath);
|
|
65
|
-
} catch {
|
|
66
|
-
const dir = dirname(filePath);
|
|
67
|
-
return dir === filePath ? filePath : join(realFilePath(dir), basename(filePath));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function pinWithinProject(base, relativePath) {
|
|
71
|
-
const checked = resolveWithinProject(base, relativePath);
|
|
72
|
-
if (!checked) return null;
|
|
73
|
-
const inside = relative(realFilePath(base), realFilePath(checked));
|
|
74
|
-
const escapes = inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
|
|
75
|
-
return escapes ? null : join(base, inside);
|
|
76
|
-
}
|
|
77
|
-
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
78
|
-
".thumbnails",
|
|
79
|
-
".transcode-cache",
|
|
80
|
-
".waveform-cache",
|
|
81
|
-
"node_modules",
|
|
82
|
-
".git"
|
|
83
|
-
]);
|
|
84
|
-
function shouldIgnoreDir(rel) {
|
|
85
|
-
return rel === ".hyperframes/backup";
|
|
86
|
-
}
|
|
87
|
-
function isInHiddenOrVendorDir(relPath) {
|
|
88
|
-
const segments = relPath.split("/");
|
|
89
|
-
return segments.slice(0, -1).some((seg) => seg.startsWith(".") || seg === "node_modules");
|
|
90
|
-
}
|
|
91
|
-
var UNREADABLE_DIR_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM", "ENOENT", "ENOTDIR"]);
|
|
92
|
-
function walkDir(dir, prefix = "") {
|
|
93
|
-
let entries;
|
|
94
|
-
try {
|
|
95
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
96
|
-
} catch (err) {
|
|
97
|
-
const code = err.code;
|
|
98
|
-
if (!prefix || !code || !UNREADABLE_DIR_CODES.has(code)) throw err;
|
|
99
|
-
return [];
|
|
100
|
-
}
|
|
101
|
-
const files = [];
|
|
102
|
-
for (const entry of entries) {
|
|
103
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
104
|
-
if (IGNORE_DIRS.has(entry.name) || shouldIgnoreDir(rel)) continue;
|
|
105
|
-
if (entry.isDirectory()) {
|
|
106
|
-
files.push(...walkDir(join(dir, entry.name), rel));
|
|
107
|
-
} else {
|
|
108
|
-
files.push(rel);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return files;
|
|
112
|
-
}
|
|
62
|
+
import { join as join2 } from "path";
|
|
113
63
|
|
|
114
64
|
// src/helpers/hfIdPersist.ts
|
|
115
65
|
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
@@ -123,7 +73,7 @@ import {
|
|
|
123
73
|
readFileSync,
|
|
124
74
|
writeFileSync
|
|
125
75
|
} from "fs";
|
|
126
|
-
import { dirname
|
|
76
|
+
import { dirname, join } from "path";
|
|
127
77
|
|
|
128
78
|
// src/helpers/atomicFile.ts
|
|
129
79
|
import * as fs from "fs";
|
|
@@ -145,10 +95,10 @@ function replaceFileAtomically(filePath, content, mode, operations = fs) {
|
|
|
145
95
|
|
|
146
96
|
// src/helpers/hfIdPersist.ts
|
|
147
97
|
var isCompositionSource = (html) => /data-composition-id\s*=/.test(html);
|
|
148
|
-
var STAMP_RECORD =
|
|
98
|
+
var STAMP_RECORD = join(".hyperframes", "hf-ids-stamped.json");
|
|
149
99
|
var contentHash = (text2) => createHash("sha1").update(text2).digest("base64url");
|
|
150
100
|
function stampProjectHfIds(projectDir) {
|
|
151
|
-
const recordPath =
|
|
101
|
+
const recordPath = join(projectDir, STAMP_RECORD);
|
|
152
102
|
let last = {};
|
|
153
103
|
try {
|
|
154
104
|
last = JSON.parse(readFileSync(recordPath, "utf-8"));
|
|
@@ -157,7 +107,7 @@ function stampProjectHfIds(projectDir) {
|
|
|
157
107
|
const next = {};
|
|
158
108
|
for (const file of walkDir(projectDir)) {
|
|
159
109
|
if (!file.endsWith(".html") || isInHiddenOrVendorDir(file)) continue;
|
|
160
|
-
const absPath =
|
|
110
|
+
const absPath = join(projectDir, file);
|
|
161
111
|
let html;
|
|
162
112
|
try {
|
|
163
113
|
html = readFileSync(absPath, "utf-8");
|
|
@@ -173,7 +123,7 @@ function stampProjectHfIds(projectDir) {
|
|
|
173
123
|
next[file] = hash;
|
|
174
124
|
}
|
|
175
125
|
try {
|
|
176
|
-
mkdirSync(
|
|
126
|
+
mkdirSync(dirname(recordPath), { recursive: true });
|
|
177
127
|
writeFileSync(recordPath, JSON.stringify(next));
|
|
178
128
|
} catch {
|
|
179
129
|
}
|
|
@@ -226,8 +176,8 @@ function stampFileHfIds(filePath) {
|
|
|
226
176
|
|
|
227
177
|
// src/helpers/projectSignature.ts
|
|
228
178
|
import { createHash as createHash2 } from "crypto";
|
|
229
|
-
import { lstatSync, readFileSync as readFileSync2, readdirSync
|
|
230
|
-
import { extname, isAbsolute
|
|
179
|
+
import { lstatSync, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
180
|
+
import { extname, isAbsolute, relative, resolve, sep } from "path";
|
|
231
181
|
var SIGNATURE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
232
182
|
".cjs",
|
|
233
183
|
".css",
|
|
@@ -262,18 +212,18 @@ var STUDIO_SIGNATURE_MANIFEST_PATHS = [
|
|
|
262
212
|
".hyperframes/studio-motion.json"
|
|
263
213
|
];
|
|
264
214
|
function affectsProjectSignature(projectDir, changedPath) {
|
|
265
|
-
const relativePath =
|
|
266
|
-
if (relativePath === "" || relativePath.startsWith("..") ||
|
|
215
|
+
const relativePath = relative(resolve(projectDir), resolve(changedPath));
|
|
216
|
+
if (relativePath === "" || relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
267
217
|
return false;
|
|
268
218
|
}
|
|
269
|
-
const segments = relativePath.split(
|
|
219
|
+
const segments = relativePath.split(sep);
|
|
270
220
|
if (STUDIO_SIGNATURE_MANIFEST_PATHS.includes(segments.join("/"))) return true;
|
|
271
221
|
return !segments.some((segment) => SIGNATURE_EXCLUDED_DIRS.has(segment));
|
|
272
222
|
}
|
|
273
223
|
var projectSignatureCache = /* @__PURE__ */ new Map();
|
|
274
224
|
function isPathWithin(parentDir, childPath) {
|
|
275
|
-
const childRelativePath =
|
|
276
|
-
return childRelativePath === "" || !childRelativePath.startsWith("..") && !
|
|
225
|
+
const childRelativePath = relative(parentDir, childPath);
|
|
226
|
+
return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute(childRelativePath);
|
|
277
227
|
}
|
|
278
228
|
function isTextContentEligible(file, size) {
|
|
279
229
|
return SIGNATURE_TEXT_EXTENSIONS.has(extname(file).toLowerCase()) && size <= MAX_SIGNATURE_TEXT_BYTES;
|
|
@@ -281,7 +231,7 @@ function isTextContentEligible(file, size) {
|
|
|
281
231
|
function collectProjectSignatureFiles(projectDir, dir, files) {
|
|
282
232
|
let entries;
|
|
283
233
|
try {
|
|
284
|
-
entries =
|
|
234
|
+
entries = readdirSync(dir).sort();
|
|
285
235
|
} catch {
|
|
286
236
|
return;
|
|
287
237
|
}
|
|
@@ -289,22 +239,22 @@ function collectProjectSignatureFiles(projectDir, dir, files) {
|
|
|
289
239
|
if (SIGNATURE_EXCLUDED_DIRS.has(entry)) continue;
|
|
290
240
|
const file = resolve(dir, entry);
|
|
291
241
|
if (!isPathWithin(projectDir, file)) continue;
|
|
292
|
-
let
|
|
242
|
+
let stat3;
|
|
293
243
|
try {
|
|
294
|
-
|
|
244
|
+
stat3 = lstatSync(file);
|
|
295
245
|
} catch {
|
|
296
246
|
continue;
|
|
297
247
|
}
|
|
298
|
-
if (
|
|
299
|
-
if (
|
|
248
|
+
if (stat3.isSymbolicLink()) continue;
|
|
249
|
+
if (stat3.isDirectory()) {
|
|
300
250
|
collectProjectSignatureFiles(projectDir, file, files);
|
|
301
|
-
} else if (
|
|
251
|
+
} else if (stat3.isFile()) {
|
|
302
252
|
files.push({
|
|
303
253
|
file,
|
|
304
|
-
mtimeMs:
|
|
305
|
-
ctimeMs:
|
|
306
|
-
size:
|
|
307
|
-
textContentEligible: isTextContentEligible(file,
|
|
254
|
+
mtimeMs: stat3.mtimeMs,
|
|
255
|
+
ctimeMs: stat3.ctimeMs,
|
|
256
|
+
size: stat3.size,
|
|
257
|
+
textContentEligible: isTextContentEligible(file, stat3.size)
|
|
308
258
|
});
|
|
309
259
|
}
|
|
310
260
|
}
|
|
@@ -314,19 +264,19 @@ function collectProjectSignatureManifestFiles(projectDir, files) {
|
|
|
314
264
|
for (const manifestPath of STUDIO_SIGNATURE_MANIFEST_PATHS) {
|
|
315
265
|
const file = resolve(projectDir, manifestPath);
|
|
316
266
|
if (seen.has(file) || !isPathWithin(projectDir, file)) continue;
|
|
317
|
-
let
|
|
267
|
+
let stat3;
|
|
318
268
|
try {
|
|
319
|
-
|
|
269
|
+
stat3 = lstatSync(file);
|
|
320
270
|
} catch {
|
|
321
271
|
continue;
|
|
322
272
|
}
|
|
323
|
-
if (
|
|
273
|
+
if (stat3.isSymbolicLink() || !stat3.isFile()) continue;
|
|
324
274
|
files.push({
|
|
325
275
|
file,
|
|
326
|
-
mtimeMs:
|
|
327
|
-
ctimeMs:
|
|
328
|
-
size:
|
|
329
|
-
textContentEligible: isTextContentEligible(file,
|
|
276
|
+
mtimeMs: stat3.mtimeMs,
|
|
277
|
+
ctimeMs: stat3.ctimeMs,
|
|
278
|
+
size: stat3.size,
|
|
279
|
+
textContentEligible: isTextContentEligible(file, stat3.size)
|
|
330
280
|
});
|
|
331
281
|
seen.add(file);
|
|
332
282
|
}
|
|
@@ -334,7 +284,7 @@ function collectProjectSignatureManifestFiles(projectDir, files) {
|
|
|
334
284
|
function createProjectFingerprint(projectDir, files) {
|
|
335
285
|
const hash = createHash2("sha256");
|
|
336
286
|
for (const entry of files) {
|
|
337
|
-
hash.update(
|
|
287
|
+
hash.update(relative(projectDir, entry.file));
|
|
338
288
|
hash.update("\0");
|
|
339
289
|
hash.update(String(entry.size));
|
|
340
290
|
hash.update("\0");
|
|
@@ -356,7 +306,7 @@ function collectProjectFiles(normalizedProjectDir) {
|
|
|
356
306
|
function listProjectFiles(projectDir) {
|
|
357
307
|
const normalizedProjectDir = resolve(projectDir);
|
|
358
308
|
return collectProjectFiles(normalizedProjectDir).map((entry) => ({
|
|
359
|
-
path:
|
|
309
|
+
path: relative(normalizedProjectDir, entry.file).split(sep).join("/"),
|
|
360
310
|
size: entry.size,
|
|
361
311
|
mtimeMs: entry.mtimeMs,
|
|
362
312
|
ctimeMs: entry.ctimeMs
|
|
@@ -374,7 +324,7 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
|
|
|
374
324
|
const normalizedProjectDir = resolve(projectDir);
|
|
375
325
|
const collected = collectProjectFiles(normalizedProjectDir);
|
|
376
326
|
const files = collected.filter(
|
|
377
|
-
(entry) => !excluding.has(
|
|
327
|
+
(entry) => !excluding.has(relative(normalizedProjectDir, entry.file).split(sep).join("/"))
|
|
378
328
|
);
|
|
379
329
|
files.sort((a, b) => a.file.localeCompare(b.file));
|
|
380
330
|
const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
|
|
@@ -383,7 +333,7 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
|
|
|
383
333
|
if (cached?.fingerprint === fingerprint) return cached.signature;
|
|
384
334
|
const hash = createHash2("sha256");
|
|
385
335
|
for (const entry of files) {
|
|
386
|
-
const relativePath =
|
|
336
|
+
const relativePath = relative(normalizedProjectDir, entry.file);
|
|
387
337
|
hash.update(relativePath);
|
|
388
338
|
hash.update("\0");
|
|
389
339
|
hash.update(String(entry.size));
|
|
@@ -410,7 +360,7 @@ async function filterCompositionFiles(projectDir, files) {
|
|
|
410
360
|
const checks = await Promise.all(
|
|
411
361
|
htmlFiles.map(async (f) => {
|
|
412
362
|
try {
|
|
413
|
-
const content = await readFile(
|
|
363
|
+
const content = await readFile(join2(projectDir, f), "utf-8");
|
|
414
364
|
return isCompositionSource(content);
|
|
415
365
|
} catch {
|
|
416
366
|
return false;
|
|
@@ -464,9 +414,9 @@ import {
|
|
|
464
414
|
statSync as statSync3,
|
|
465
415
|
fstatSync as fstatSync2,
|
|
466
416
|
renameSync as renameSync2,
|
|
467
|
-
readdirSync as
|
|
417
|
+
readdirSync as readdirSync3
|
|
468
418
|
} from "fs";
|
|
469
|
-
import { resolve as resolve3, dirname as
|
|
419
|
+
import { resolve as resolve3, dirname as dirname4, join as join6 } from "path";
|
|
470
420
|
|
|
471
421
|
// src/helpers/mime.ts
|
|
472
422
|
var MIME_TYPES = {
|
|
@@ -526,7 +476,7 @@ import {
|
|
|
526
476
|
rmSync,
|
|
527
477
|
statSync
|
|
528
478
|
} from "fs";
|
|
529
|
-
import { dirname as
|
|
479
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
530
480
|
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
531
481
|
var SAMPLE_RATE = 4e3;
|
|
532
482
|
var PEAK_COUNT = 4e3;
|
|
@@ -593,11 +543,11 @@ function decodeAudioPeaks(audioPath) {
|
|
|
593
543
|
});
|
|
594
544
|
}
|
|
595
545
|
async function generateWaveformCache(projectDir, assetPath) {
|
|
596
|
-
const audioPath =
|
|
546
|
+
const audioPath = join3(projectDir, assetPath);
|
|
597
547
|
if (!existsSync(audioPath)) return;
|
|
598
548
|
const stats = statSync(audioPath);
|
|
599
|
-
const cacheDir =
|
|
600
|
-
const cachePath =
|
|
549
|
+
const cacheDir = join3(projectDir, ".waveform-cache");
|
|
550
|
+
const cachePath = join3(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
601
551
|
if (isWaveformCacheDirectory(cacheDir) && existsSync(cachePath)) return;
|
|
602
552
|
const peaks = await decodeAudioPeaks(audioPath);
|
|
603
553
|
writeWaveformCache(cachePath, peaks);
|
|
@@ -606,7 +556,7 @@ function isWaveformCacheDirectory(cacheDir) {
|
|
|
606
556
|
return lstatSync2(cacheDir, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
607
557
|
}
|
|
608
558
|
function writeWaveformCache(cachePath, peaks) {
|
|
609
|
-
const cacheDir =
|
|
559
|
+
const cacheDir = dirname2(cachePath);
|
|
610
560
|
try {
|
|
611
561
|
mkdirSync2(cacheDir, { mode: 448 });
|
|
612
562
|
} catch (error) {
|
|
@@ -615,9 +565,9 @@ function writeWaveformCache(cachePath, peaks) {
|
|
|
615
565
|
if (!isWaveformCacheDirectory(cacheDir)) {
|
|
616
566
|
throw new Error("Waveform cache must be a directory, not a symlink");
|
|
617
567
|
}
|
|
618
|
-
const stagingDir = mkdtempSync(
|
|
568
|
+
const stagingDir = mkdtempSync(join3(cacheDir, ".waveform-"));
|
|
619
569
|
try {
|
|
620
|
-
const stagingPath =
|
|
570
|
+
const stagingPath = join3(stagingDir, "peaks.json");
|
|
621
571
|
writeFileSync2(stagingPath, JSON.stringify(peaks), { flag: "wx" });
|
|
622
572
|
renameSync(stagingPath, cachePath);
|
|
623
573
|
} finally {
|
|
@@ -632,7 +582,7 @@ function writeWaveformCache(cachePath, peaks) {
|
|
|
632
582
|
import { spawnSync } from "child_process";
|
|
633
583
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
634
584
|
import { tmpdir } from "os";
|
|
635
|
-
import { basename
|
|
585
|
+
import { basename, join as join4 } from "path";
|
|
636
586
|
var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
|
|
637
587
|
var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
|
|
638
588
|
function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
@@ -669,8 +619,8 @@ function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
|
669
619
|
}
|
|
670
620
|
}
|
|
671
621
|
function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
|
|
672
|
-
const tempDir = mkdtempSync2(
|
|
673
|
-
const tempPath =
|
|
622
|
+
const tempDir = mkdtempSync2(join4(tmpdir(), "hyperframes-upload-"));
|
|
623
|
+
const tempPath = join4(tempDir, basename(fileName));
|
|
674
624
|
try {
|
|
675
625
|
writeFileSync3(tempPath, buffer);
|
|
676
626
|
return validateUploadedMedia(tempPath, runner);
|
|
@@ -680,9 +630,9 @@ function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
|
|
|
680
630
|
}
|
|
681
631
|
|
|
682
632
|
// src/helpers/backupJournal.ts
|
|
683
|
-
import { mkdirSync as mkdirSync3, readdirSync as
|
|
633
|
+
import { mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
684
634
|
import { Buffer as Buffer2 } from "buffer";
|
|
685
|
-
import { join as
|
|
635
|
+
import { join as join5, relative as relative2 } from "path";
|
|
686
636
|
var DEFAULT_KEEP_PER_FILE = 10;
|
|
687
637
|
function backupKeyForPath(path) {
|
|
688
638
|
return Buffer2.from(path, "utf-8").toString("base64url");
|
|
@@ -692,7 +642,7 @@ function timestampPrefix() {
|
|
|
692
642
|
}
|
|
693
643
|
function backupPathForResponse(projectDir, backupPath) {
|
|
694
644
|
if (!backupPath) return null;
|
|
695
|
-
const rel =
|
|
645
|
+
const rel = relative2(projectDir, backupPath);
|
|
696
646
|
if (!rel || rel.startsWith("..")) return null;
|
|
697
647
|
return rel.split("\\").join("/");
|
|
698
648
|
}
|
|
@@ -700,8 +650,8 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
|
|
|
700
650
|
if (!isSafePath(projectDir, absPath)) return { backupPath: null };
|
|
701
651
|
try {
|
|
702
652
|
const content = readFileSync3(absPath);
|
|
703
|
-
const relativePath =
|
|
704
|
-
const backupDir =
|
|
653
|
+
const relativePath = relative2(projectDir, absPath);
|
|
654
|
+
const backupDir = join5(projectDir, ".hyperframes", "backup");
|
|
705
655
|
mkdirSync3(backupDir, { recursive: true });
|
|
706
656
|
const backupKey = backupKeyForPath(relativePath);
|
|
707
657
|
const backupPath = nextBackupPath(backupDir, backupKey);
|
|
@@ -717,7 +667,7 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
|
|
|
717
667
|
}
|
|
718
668
|
function nextBackupPath(backupDir, backupKey) {
|
|
719
669
|
const base = `${timestampPrefix()}-${backupKey}`;
|
|
720
|
-
let candidate =
|
|
670
|
+
let candidate = join5(backupDir, base);
|
|
721
671
|
let counter = 2;
|
|
722
672
|
while (true) {
|
|
723
673
|
try {
|
|
@@ -728,7 +678,7 @@ function nextBackupPath(backupDir, backupKey) {
|
|
|
728
678
|
}
|
|
729
679
|
throw error;
|
|
730
680
|
}
|
|
731
|
-
candidate =
|
|
681
|
+
candidate = join5(backupDir, `${base}-${counter}`);
|
|
732
682
|
counter += 1;
|
|
733
683
|
}
|
|
734
684
|
}
|
|
@@ -736,7 +686,7 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
|
|
|
736
686
|
const keep = Math.max(1, Math.floor(keepPerFile));
|
|
737
687
|
const suffix = `-${backupKey}`;
|
|
738
688
|
const numberedSuffix = new RegExp(`-${backupKey}-\\d+$`);
|
|
739
|
-
const matches =
|
|
689
|
+
const matches = readdirSync2(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join5(backupDir, name)).sort((a, b) => {
|
|
740
690
|
return b.localeCompare(a);
|
|
741
691
|
});
|
|
742
692
|
for (const file of matches.slice(keep)) {
|
|
@@ -761,9 +711,9 @@ function hashVersion(hex) {
|
|
|
761
711
|
function hashOfVersion(version) {
|
|
762
712
|
return /^"sha256:([0-9a-f]{64})"$/.exec(version)?.[1];
|
|
763
713
|
}
|
|
764
|
-
function settledFileTag(
|
|
765
|
-
if (now -
|
|
766
|
-
return [
|
|
714
|
+
function settledFileTag(stat3, now = Date.now()) {
|
|
715
|
+
if (now - stat3.ctimeMs < 3e3) return null;
|
|
716
|
+
return [stat3.ino, stat3.ctimeMs, stat3.size].map((n) => n.toString(36)).join("-");
|
|
767
717
|
}
|
|
768
718
|
function createWriteToken(requestToken) {
|
|
769
719
|
const token = requestToken?.trim();
|
|
@@ -937,9 +887,9 @@ import { parseHTML as parseHTML2 } from "linkedom";
|
|
|
937
887
|
import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
|
|
938
888
|
|
|
939
889
|
// src/helpers/compositionInsertion.ts
|
|
940
|
-
import { existsSync as existsSync2, readFileSync as readFileSync5
|
|
890
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
941
891
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
942
|
-
import { dirname as
|
|
892
|
+
import { dirname as dirname3, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
|
|
943
893
|
import { parseHTML } from "linkedom";
|
|
944
894
|
var CompositionInsertionError = class extends Error {
|
|
945
895
|
constructor(message, status) {
|
|
@@ -975,8 +925,8 @@ function canonicalProjectPath(projectDir, candidate) {
|
|
|
975
925
|
if (!existsSync2(candidate)) {
|
|
976
926
|
throw new CompositionInsertionError("Composition source was not found", 404);
|
|
977
927
|
}
|
|
978
|
-
const canonical =
|
|
979
|
-
if (!isSafePath(
|
|
928
|
+
const canonical = realpath(candidate);
|
|
929
|
+
if (!isSafePath(realpath(projectDir), canonical)) {
|
|
980
930
|
throw new CompositionInsertionError("Composition source escapes the project", 400);
|
|
981
931
|
}
|
|
982
932
|
return canonical;
|
|
@@ -988,13 +938,13 @@ function validateSourcePath(sourcePath) {
|
|
|
988
938
|
}
|
|
989
939
|
function canonicalProjectFile(projectDir, sourcePath) {
|
|
990
940
|
validateSourcePath(sourcePath);
|
|
991
|
-
return canonicalProjectPath(projectDir,
|
|
941
|
+
return canonicalProjectPath(projectDir, resolveWithinProject(projectDir, sourcePath));
|
|
992
942
|
}
|
|
993
943
|
function canonicalDependency(projectDir, ownerAbs, sourcePath) {
|
|
994
944
|
validateSourcePath(sourcePath);
|
|
995
945
|
return canonicalProjectPath(
|
|
996
946
|
projectDir,
|
|
997
|
-
|
|
947
|
+
resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname3(ownerAbs), sourcePath)))
|
|
998
948
|
);
|
|
999
949
|
}
|
|
1000
950
|
function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
|
|
@@ -1067,7 +1017,7 @@ function uniqueHostId(root, base) {
|
|
|
1067
1017
|
return `${base}_${suffix}`;
|
|
1068
1018
|
}
|
|
1069
1019
|
function relativeSourcePath(targetAbs, sourceAbs) {
|
|
1070
|
-
return
|
|
1020
|
+
return relative3(dirname3(targetAbs), sourceAbs).split(sep2).join("/");
|
|
1071
1021
|
}
|
|
1072
1022
|
function insertCompositionIntoSource(input) {
|
|
1073
1023
|
const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
|
|
@@ -1199,7 +1149,7 @@ async function loadGsapParser() {
|
|
|
1199
1149
|
return import("@hyperframes/parsers/gsap-parser-recast");
|
|
1200
1150
|
}
|
|
1201
1151
|
function isDanglingSymlinkInProject(projectDir, lexicalPath) {
|
|
1202
|
-
if (!isSafePath(projectDir,
|
|
1152
|
+
if (!isSafePath(projectDir, dirname4(lexicalPath))) return false;
|
|
1203
1153
|
let stats;
|
|
1204
1154
|
try {
|
|
1205
1155
|
stats = lstatSync3(lexicalPath);
|
|
@@ -1207,7 +1157,7 @@ function isDanglingSymlinkInProject(projectDir, lexicalPath) {
|
|
|
1207
1157
|
return false;
|
|
1208
1158
|
}
|
|
1209
1159
|
if (!stats.isSymbolicLink()) return false;
|
|
1210
|
-
const target = resolve3(
|
|
1160
|
+
const target = resolve3(dirname4(lexicalPath), readlinkSync(lexicalPath));
|
|
1211
1161
|
if (!isSafePath(projectDir, target)) return false;
|
|
1212
1162
|
try {
|
|
1213
1163
|
statSync3(lexicalPath);
|
|
@@ -1231,7 +1181,7 @@ async function resolveProjectPath(c, adapter, route, opts) {
|
|
|
1231
1181
|
if (filePath.includes("\0")) {
|
|
1232
1182
|
return { error: c.json({ error: "forbidden", why: "nul" }, 403) };
|
|
1233
1183
|
}
|
|
1234
|
-
const absPath = (opts?.pin ? pinWithinProject :
|
|
1184
|
+
const absPath = (opts?.pin ? pinWithinProject : resolveWithinProject)(project.dir, filePath);
|
|
1235
1185
|
if (!absPath) {
|
|
1236
1186
|
if (isDanglingSymlinkInProject(project.dir, resolve3(project.dir, filePath))) {
|
|
1237
1187
|
return { error: c.json({ error: "not found", why: "dangling_symlink" }, 404) };
|
|
@@ -1430,7 +1380,7 @@ async function parseMutationBody(c) {
|
|
|
1430
1380
|
return { target: body.target, body };
|
|
1431
1381
|
}
|
|
1432
1382
|
function ensureDir(filePath) {
|
|
1433
|
-
const dir =
|
|
1383
|
+
const dir = dirname4(filePath);
|
|
1434
1384
|
if (!existsSync3(dir)) mkdirSync4(dir, { recursive: true });
|
|
1435
1385
|
}
|
|
1436
1386
|
function generateCopyPath(projectDir, originalPath) {
|
|
@@ -1448,8 +1398,8 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1448
1398
|
}
|
|
1449
1399
|
function walkFiles(dir, filter) {
|
|
1450
1400
|
const results = [];
|
|
1451
|
-
for (const entry of
|
|
1452
|
-
const full =
|
|
1401
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
1402
|
+
const full = join6(dir, entry.name);
|
|
1453
1403
|
if (entry.isDirectory()) {
|
|
1454
1404
|
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders" || entry.name === ".transcode-cache")
|
|
1455
1405
|
continue;
|
|
@@ -2612,7 +2562,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2612
2562
|
if (n >= MAX_COPY_INDEX) skipped.push(name);
|
|
2613
2563
|
continue;
|
|
2614
2564
|
}
|
|
2615
|
-
const relativePath = subDir ?
|
|
2565
|
+
const relativePath = subDir ? join6(subDir, finalName) : finalName;
|
|
2616
2566
|
uploaded.push(relativePath);
|
|
2617
2567
|
if (isAudioFile(finalName)) {
|
|
2618
2568
|
generateWaveformCache(projectDir, relativePath).catch(() => {
|
|
@@ -2776,10 +2726,10 @@ function registerFileRoutes(api, adapter) {
|
|
|
2776
2726
|
api.delete("/projects/:id/files/*", async (c) => {
|
|
2777
2727
|
const res = await resolveProjectFile(c, adapter, { mustExist: true });
|
|
2778
2728
|
if ("error" in res) return res.error;
|
|
2779
|
-
const
|
|
2729
|
+
const stat3 = statSync3(res.absPath);
|
|
2780
2730
|
const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
2781
2731
|
if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
|
|
2782
|
-
if (
|
|
2732
|
+
if (stat3.isDirectory()) {
|
|
2783
2733
|
rmSync3(res.absPath, { recursive: true });
|
|
2784
2734
|
} else {
|
|
2785
2735
|
unlinkSync2(res.absPath);
|
|
@@ -3295,7 +3245,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3295
3245
|
if (!body.newPath || body.newPath.includes("\0")) {
|
|
3296
3246
|
return c.json({ error: "newPath required" }, 400);
|
|
3297
3247
|
}
|
|
3298
|
-
const newAbs =
|
|
3248
|
+
const newAbs = resolveWithinProject(res.project.dir, body.newPath);
|
|
3299
3249
|
if (!newAbs) {
|
|
3300
3250
|
return c.json({ error: "forbidden" }, 403);
|
|
3301
3251
|
}
|
|
@@ -3314,12 +3264,12 @@ function registerFileRoutes(api, adapter) {
|
|
|
3314
3264
|
if (!body.path || body.path.includes("\0")) {
|
|
3315
3265
|
return c.json({ error: "path required" }, 400);
|
|
3316
3266
|
}
|
|
3317
|
-
const srcAbs =
|
|
3267
|
+
const srcAbs = resolveWithinProject(project.dir, body.path);
|
|
3318
3268
|
if (!srcAbs || !existsSync3(srcAbs)) {
|
|
3319
3269
|
return c.json({ error: "not found" }, 404);
|
|
3320
3270
|
}
|
|
3321
3271
|
const copyPath = generateCopyPath(project.dir, body.path);
|
|
3322
|
-
const destAbs =
|
|
3272
|
+
const destAbs = resolveWithinProject(project.dir, copyPath);
|
|
3323
3273
|
if (!destAbs) {
|
|
3324
3274
|
return c.json({ error: "forbidden" }, 403);
|
|
3325
3275
|
}
|
|
@@ -3345,7 +3295,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3345
3295
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
3346
3296
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
3347
3297
|
const subDir = c.req.query("dir") ?? "";
|
|
3348
|
-
const targetDir = subDir ?
|
|
3298
|
+
const targetDir = subDir ? resolveWithinProject(project.dir, subDir) : project.dir;
|
|
3349
3299
|
if (!targetDir) return c.json({ error: "forbidden" }, 403);
|
|
3350
3300
|
if (subDir && !existsSync3(targetDir)) mkdirSync4(targetDir, { recursive: true });
|
|
3351
3301
|
const formData = await c.req.formData();
|
|
@@ -3429,7 +3379,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3429
3379
|
// src/routes/preview.ts
|
|
3430
3380
|
import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
|
|
3431
3381
|
import { Readable } from "stream";
|
|
3432
|
-
import { join as join9, resolve as
|
|
3382
|
+
import { join as join9, resolve as resolve5 } from "path";
|
|
3433
3383
|
import { createHash as createHash4 } from "crypto";
|
|
3434
3384
|
import {
|
|
3435
3385
|
addScenePartsManifest,
|
|
@@ -3441,7 +3391,7 @@ import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
|
3441
3391
|
|
|
3442
3392
|
// src/helpers/subComposition.ts
|
|
3443
3393
|
import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
|
|
3444
|
-
import { join as
|
|
3394
|
+
import { join as join7 } from "path";
|
|
3445
3395
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
3446
3396
|
import {
|
|
3447
3397
|
rewriteAssetPaths,
|
|
@@ -3451,7 +3401,7 @@ import {
|
|
|
3451
3401
|
import { stripEmbeddedRuntimeScripts } from "@hyperframes/core/compiler";
|
|
3452
3402
|
import { isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
|
|
3453
3403
|
function rewriteRelativePaths(root, compPath, projectDir) {
|
|
3454
|
-
const assetExists = (path) => existsSync4(
|
|
3404
|
+
const assetExists = (path) => existsSync4(join7(projectDir, path));
|
|
3455
3405
|
rewriteAssetPaths(
|
|
3456
3406
|
root.querySelectorAll("[src], [href]"),
|
|
3457
3407
|
compPath,
|
|
@@ -3548,7 +3498,7 @@ function tagRootCompositionFile(bodyHtml, compPath) {
|
|
|
3548
3498
|
return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
|
|
3549
3499
|
}
|
|
3550
3500
|
function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
|
|
3551
|
-
const compFile =
|
|
3501
|
+
const compFile = join7(projectDir, compPath);
|
|
3552
3502
|
if (!existsSync4(compFile)) return null;
|
|
3553
3503
|
const rawComp = rawOverride ?? readFileSync7(compFile, "utf-8");
|
|
3554
3504
|
let compHeadContent = "";
|
|
@@ -3580,7 +3530,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
3580
3530
|
}
|
|
3581
3531
|
rewrittenContent = stripEmbeddedRuntimeScripts(rewrittenContent);
|
|
3582
3532
|
rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
|
|
3583
|
-
const indexPath =
|
|
3533
|
+
const indexPath = join7(projectDir, "index.html");
|
|
3584
3534
|
let headContent = "";
|
|
3585
3535
|
if (existsSync4(indexPath)) {
|
|
3586
3536
|
const indexHtml = readFileSync7(indexPath, "utf-8");
|
|
@@ -3622,6 +3572,83 @@ function hasBaseElement(html) {
|
|
|
3622
3572
|
// src/routes/preview.ts
|
|
3623
3573
|
import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
|
|
3624
3574
|
|
|
3575
|
+
// src/helpers/previewReads.ts
|
|
3576
|
+
import { readlinkSync as readlinkSync2 } from "fs";
|
|
3577
|
+
import { dirname as dirname5, join as join8, parse, relative as relative4, resolve as resolve4 } from "path";
|
|
3578
|
+
var ALWAYS_AFFECTS = ["hyperframes.json", ...STUDIO_SIGNATURE_MANIFEST_PATHS];
|
|
3579
|
+
var REFERENCE = /\b(?:src|href|poster|data-composition-src)\s*=\s*(?:"([^"\n]*)"|'([^'\n]*)')|url\(\s*(?:"([^"\n]*)"|'([^'\n]*)'|([^"'\s)]+))/gi;
|
|
3580
|
+
var pathKey = process.platform === "darwin" || process.platform === "win32" ? (path) => resolve4(path).toLowerCase() : (path) => resolve4(path);
|
|
3581
|
+
var readsByProject = /* @__PURE__ */ new Map();
|
|
3582
|
+
var builtProjects = /* @__PURE__ */ new Set();
|
|
3583
|
+
var projectKey = (projectDir) => pathKey(realFilePath(resolve4(projectDir)));
|
|
3584
|
+
var inRealProject = (projectDir, path) => join8(realFilePath(resolve4(projectDir)), relative4(resolve4(projectDir), resolve4(projectDir, path)));
|
|
3585
|
+
var readLink = (path) => {
|
|
3586
|
+
try {
|
|
3587
|
+
return readlinkSync2(path);
|
|
3588
|
+
} catch {
|
|
3589
|
+
return null;
|
|
3590
|
+
}
|
|
3591
|
+
};
|
|
3592
|
+
function linksOnTheWay(path) {
|
|
3593
|
+
const passed = [];
|
|
3594
|
+
let at = parse(path).root;
|
|
3595
|
+
let rest = path.slice(at.length).split(/[\\/]+/);
|
|
3596
|
+
for (let hops = 0; rest.length > 0; ) {
|
|
3597
|
+
const part = rest.shift();
|
|
3598
|
+
if (!part || part === ".") continue;
|
|
3599
|
+
if (part === "..") {
|
|
3600
|
+
at = dirname5(at);
|
|
3601
|
+
continue;
|
|
3602
|
+
}
|
|
3603
|
+
const next = join8(at, part);
|
|
3604
|
+
const target = hops < 40 ? readLink(next) : null;
|
|
3605
|
+
if (target === null) {
|
|
3606
|
+
at = next;
|
|
3607
|
+
continue;
|
|
3608
|
+
}
|
|
3609
|
+
hops++;
|
|
3610
|
+
passed.push(next);
|
|
3611
|
+
const root = parse(target).root;
|
|
3612
|
+
if (root) at = root;
|
|
3613
|
+
rest = [...target.slice(root.length).split(/[\\/]+/), ...rest];
|
|
3614
|
+
}
|
|
3615
|
+
passed.push(at);
|
|
3616
|
+
return passed;
|
|
3617
|
+
}
|
|
3618
|
+
function recordPreviewRead(projectDir, filePath) {
|
|
3619
|
+
const key = projectKey(projectDir);
|
|
3620
|
+
let reads = readsByProject.get(key);
|
|
3621
|
+
if (!reads) readsByProject.set(key, reads = /* @__PURE__ */ new Set());
|
|
3622
|
+
const read = inRealProject(projectDir, filePath);
|
|
3623
|
+
for (const found of [read, ...linksOnTheWay(read)]) {
|
|
3624
|
+
for (let path = pathKey(found); !reads.has(path); path = dirname5(path)) reads.add(path);
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
function recordPreviewReferences(projectDir, html) {
|
|
3628
|
+
const named = /* @__PURE__ */ new Set();
|
|
3629
|
+
for (const match of html.matchAll(REFERENCE)) {
|
|
3630
|
+
const url = (match[1] ?? match[2] ?? match[3] ?? match[4] ?? match[5] ?? "").trim();
|
|
3631
|
+
if (!url || /^(?:[a-z][a-z0-9+.-]*:|[/#])/i.test(url)) continue;
|
|
3632
|
+
const path = url.split(/[?#]/)[0] ?? "";
|
|
3633
|
+
let decoded = path;
|
|
3634
|
+
try {
|
|
3635
|
+
decoded = decodeURIComponent(path);
|
|
3636
|
+
} catch {
|
|
3637
|
+
}
|
|
3638
|
+
if (decoded) named.add(decoded);
|
|
3639
|
+
}
|
|
3640
|
+
for (const path of named) recordPreviewRead(projectDir, path);
|
|
3641
|
+
}
|
|
3642
|
+
function recordPreviewBuilt(projectDir) {
|
|
3643
|
+
for (const path of ALWAYS_AFFECTS) recordPreviewRead(projectDir, path);
|
|
3644
|
+
builtProjects.add(projectKey(projectDir));
|
|
3645
|
+
}
|
|
3646
|
+
function affectsPreview(projectDir, changedPath) {
|
|
3647
|
+
const key = projectKey(projectDir);
|
|
3648
|
+
if (!builtProjects.has(key)) return true;
|
|
3649
|
+
return readsByProject.get(key)?.has(pathKey(inRealProject(projectDir, changedPath))) ?? false;
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3625
3652
|
// src/helpers/variablesPayload.ts
|
|
3626
3653
|
var VARIABLES_PAYLOAD_ERROR = "variables must be a JSON object of {variableId: value}";
|
|
3627
3654
|
function isVariablesPayload(value) {
|
|
@@ -3779,9 +3806,9 @@ function variablesEtagSalt(raw) {
|
|
|
3779
3806
|
return `:vars:${createHash4("sha1").update(raw).digest("hex").slice(0, 12)}`;
|
|
3780
3807
|
}
|
|
3781
3808
|
function previewVariablesFromRequest(rawVariables) {
|
|
3782
|
-
const
|
|
3783
|
-
if (!
|
|
3784
|
-
return { raw: rawVariables, values:
|
|
3809
|
+
const parse2 = parsePreviewVariablesParam(rawVariables);
|
|
3810
|
+
if (!parse2.ok) return { error: parse2.error };
|
|
3811
|
+
return { raw: rawVariables, values: parse2.values };
|
|
3785
3812
|
}
|
|
3786
3813
|
function injectStudioPreviewAugmentations(html, adapter, projectDir, activeCompositionPath) {
|
|
3787
3814
|
return injectStudioMotionScript(
|
|
@@ -3809,13 +3836,15 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
|
|
|
3809
3836
|
}
|
|
3810
3837
|
function resolveProjectMainHtml(projectDir, projectId) {
|
|
3811
3838
|
const indexPath = join9(projectDir, "index.html");
|
|
3839
|
+
const blockHtmlPath = join9(projectDir, `${projectId}.html`);
|
|
3840
|
+
recordPreviewRead(projectDir, indexPath);
|
|
3841
|
+
recordPreviewRead(projectDir, blockHtmlPath);
|
|
3812
3842
|
if (existsSync5(indexPath)) {
|
|
3813
3843
|
return {
|
|
3814
3844
|
html: readFileSync8(indexPath, "utf-8"),
|
|
3815
3845
|
compositionPath: "index.html"
|
|
3816
3846
|
};
|
|
3817
3847
|
}
|
|
3818
|
-
const blockHtmlPath = join9(projectDir, `${projectId}.html`);
|
|
3819
3848
|
if (existsSync5(blockHtmlPath)) {
|
|
3820
3849
|
return {
|
|
3821
3850
|
html: readFileSync8(blockHtmlPath, "utf-8"),
|
|
@@ -3847,13 +3876,18 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3847
3876
|
const diskMain = resolveProjectMainHtml(project.dir, project.id);
|
|
3848
3877
|
const normalizedDisk = diskMain ? ensureHfIds3(diskMain.html) : null;
|
|
3849
3878
|
try {
|
|
3850
|
-
let bundled = await adapter.bundle(project.dir, {
|
|
3879
|
+
let bundled = await adapter.bundle(project.dir, {
|
|
3880
|
+
stampHfIds: true,
|
|
3881
|
+
onRead: (filePath) => recordPreviewRead(project.dir, filePath)
|
|
3882
|
+
});
|
|
3851
3883
|
let mainCompositionPath = "index.html";
|
|
3852
3884
|
if (!bundled) {
|
|
3853
3885
|
if (!diskMain) return null;
|
|
3854
3886
|
bundled = stripEmbeddedRuntimeScripts2(normalizedDisk ?? diskMain.html);
|
|
3855
3887
|
mainCompositionPath = diskMain.compositionPath;
|
|
3856
3888
|
}
|
|
3889
|
+
recordPreviewReferences(project.dir, bundled);
|
|
3890
|
+
recordPreviewBuilt(project.dir);
|
|
3857
3891
|
if (!bundled.includes("hyperframe.runtime") && !bundled.includes("hyperframes-preview-runtime")) {
|
|
3858
3892
|
const runtimeTag = `<script src="${adapter.runtimeUrl}"></script>`;
|
|
3859
3893
|
bundled = insertBeforeCloseTag2(bundled, "body", `${runtimeTag}
|
|
@@ -3956,7 +3990,9 @@ ${etag}`;
|
|
|
3956
3990
|
if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
|
|
3957
3991
|
const previewVariables = vars.values;
|
|
3958
3992
|
const compPath = requestSubPath(c.req.url, "projects/:id/preview/comp");
|
|
3959
|
-
const compFile =
|
|
3993
|
+
const compFile = resolveWithinProject(project.dir, compPath);
|
|
3994
|
+
if (compFile) recordPreviewRead(project.dir, compFile);
|
|
3995
|
+
recordPreviewRead(project.dir, "index.html");
|
|
3960
3996
|
if (!compFile || !existsSync5(compFile) || !statSync4(compFile).isFile()) {
|
|
3961
3997
|
return c.text("not found", 404);
|
|
3962
3998
|
}
|
|
@@ -3980,6 +4016,7 @@ ${etag}`;
|
|
|
3980
4016
|
stamped
|
|
3981
4017
|
);
|
|
3982
4018
|
if (!html) return c.text("not found", 404);
|
|
4019
|
+
recordPreviewReferences(project.dir, html);
|
|
3983
4020
|
html = ensureHfIds3(await transformPreviewHtml(html, adapter, project, compPath));
|
|
3984
4021
|
html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath);
|
|
3985
4022
|
if (previewVariables) html = injectPreviewVariables(html, previewVariables);
|
|
@@ -3990,13 +4027,14 @@ ${etag}`;
|
|
|
3990
4027
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
3991
4028
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
3992
4029
|
const subPath = requestSubPath(c.req.url, "projects/:id/preview");
|
|
3993
|
-
const candidate =
|
|
4030
|
+
const candidate = resolve5(project.dir, subPath);
|
|
3994
4031
|
const file = isWithinProjectRoot(project.dir, candidate) ? candidate : null;
|
|
3995
4032
|
if (!file) {
|
|
3996
4033
|
return c.text("not found", 404);
|
|
3997
4034
|
}
|
|
3998
|
-
|
|
3999
|
-
|
|
4035
|
+
recordPreviewRead(project.dir, file);
|
|
4036
|
+
const stat3 = existsSync5(file) ? statSync4(file) : null;
|
|
4037
|
+
if (!stat3?.isFile()) {
|
|
4000
4038
|
return c.text("not found", 404);
|
|
4001
4039
|
}
|
|
4002
4040
|
const contentType = getMimeType(subPath);
|
|
@@ -4007,7 +4045,7 @@ ${etag}`;
|
|
|
4007
4045
|
if (!isProxyVariantRequest(proxyParam) || !contentType.startsWith("video/") || !isAutoProxyEnabled(adapter)) {
|
|
4008
4046
|
return c.text("not found", 404);
|
|
4009
4047
|
}
|
|
4010
|
-
const facts = await probeAssetCodec(file);
|
|
4048
|
+
const facts = await probeAssetCodec(file, void 0, mediaCodecProbeCache);
|
|
4011
4049
|
const eligibility = decideMediaProxyEligibility(facts);
|
|
4012
4050
|
if (!eligibility.eligible) {
|
|
4013
4051
|
return c.text(`media proxy unavailable: ${eligibility.reason}`, 422);
|
|
@@ -4018,7 +4056,7 @@ ${etag}`;
|
|
|
4018
4056
|
return c.text("media proxy variant does not match asset", 422);
|
|
4019
4057
|
}
|
|
4020
4058
|
}
|
|
4021
|
-
const tag = settledFileTag(
|
|
4059
|
+
const tag = settledFileTag(stat3);
|
|
4022
4060
|
const etag = tag && `"${tag}${proxyEtagSalt(proxyVariant)}"`;
|
|
4023
4061
|
const cacheHeaders = isText ? { "Cache-Control": "no-store" } : { "Cache-Control": "private, no-cache", ...etag && { ETag: etag } };
|
|
4024
4062
|
if (!isText && etag) {
|
|
@@ -4138,7 +4176,7 @@ function registerLintRoutes(api, adapter) {
|
|
|
4138
4176
|
|
|
4139
4177
|
// src/routes/render.ts
|
|
4140
4178
|
import { streamSSE } from "hono/streaming";
|
|
4141
|
-
import { existsSync as existsSync6, readFileSync as readFileSync10, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3, readdirSync as
|
|
4179
|
+
import { existsSync as existsSync6, readFileSync as readFileSync10, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
4142
4180
|
import { join as join11 } from "path";
|
|
4143
4181
|
import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
|
|
4144
4182
|
import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
|
|
@@ -4190,7 +4228,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4190
4228
|
const outputResolution = VALID_RESOLUTIONS.has(body.resolution ?? "") ? body.resolution : void 0;
|
|
4191
4229
|
let composition;
|
|
4192
4230
|
if (typeof body.composition === "string" && body.composition.length > 0) {
|
|
4193
|
-
if (!
|
|
4231
|
+
if (!resolveWithinProject(project.dir, body.composition)) {
|
|
4194
4232
|
return c.json({ error: "composition path must be within the project directory" }, 400);
|
|
4195
4233
|
}
|
|
4196
4234
|
composition = body.composition;
|
|
@@ -4323,7 +4361,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4323
4361
|
const filename = c.req.path.split("/renders/file/")[1];
|
|
4324
4362
|
if (!filename) return c.json({ error: "missing filename" }, 400);
|
|
4325
4363
|
const rendersDir = adapter.rendersDir(project);
|
|
4326
|
-
const fp =
|
|
4364
|
+
const fp = resolveWithinProject(rendersDir, filename);
|
|
4327
4365
|
if (!fp) return c.json({ error: "forbidden" }, 403);
|
|
4328
4366
|
if (!existsSync6(fp)) return c.json({ error: "not found" }, 404);
|
|
4329
4367
|
const contentType = renderContentType(fp);
|
|
@@ -4342,9 +4380,9 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4342
4380
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
4343
4381
|
const rendersDir = adapter.rendersDir(project);
|
|
4344
4382
|
if (!existsSync6(rendersDir)) return c.json({ renders: [] });
|
|
4345
|
-
const files =
|
|
4383
|
+
const files = readdirSync4(rendersDir).filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov")).map((f) => {
|
|
4346
4384
|
const fp = join11(rendersDir, f);
|
|
4347
|
-
const
|
|
4385
|
+
const stat3 = statSync5(fp);
|
|
4348
4386
|
const rid = f.replace(/\.(mp4|webm|mov)$/, "");
|
|
4349
4387
|
const metaPath = join11(rendersDir, `${rid}.meta.json`);
|
|
4350
4388
|
let status = "complete";
|
|
@@ -4360,8 +4398,8 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4360
4398
|
return {
|
|
4361
4399
|
id: rid,
|
|
4362
4400
|
filename: f,
|
|
4363
|
-
size:
|
|
4364
|
-
createdAt:
|
|
4401
|
+
size: stat3.size,
|
|
4402
|
+
createdAt: stat3.mtimeMs,
|
|
4365
4403
|
status,
|
|
4366
4404
|
durationMs
|
|
4367
4405
|
};
|
|
@@ -4381,80 +4419,11 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4381
4419
|
});
|
|
4382
4420
|
}
|
|
4383
4421
|
|
|
4384
|
-
// src/routes/
|
|
4385
|
-
import {
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
openSync as openSync3,
|
|
4390
|
-
mkdirSync as mkdirSync6,
|
|
4391
|
-
readFileSync as readFileSync12,
|
|
4392
|
-
readdirSync as readdirSync6,
|
|
4393
|
-
renameSync as renameSync3,
|
|
4394
|
-
rmSync as rmSync4,
|
|
4395
|
-
statSync as statSync6,
|
|
4396
|
-
unlinkSync as unlinkSync4,
|
|
4397
|
-
writeFileSync as writeFileSync6
|
|
4398
|
-
} from "fs";
|
|
4399
|
-
import { join as join12 } from "path";
|
|
4400
|
-
import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
|
|
4401
|
-
|
|
4402
|
-
// src/helpers/compositionInputs.ts
|
|
4403
|
-
import { readFileSync as readFileSync11 } from "fs";
|
|
4404
|
-
var ROOT_COMPOSITION = "index.html";
|
|
4405
|
-
var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
|
|
4406
|
-
function projectReader(projectDir) {
|
|
4407
|
-
const sources = /* @__PURE__ */ new Map();
|
|
4408
|
-
return (compPath) => {
|
|
4409
|
-
if (!sources.has(compPath)) {
|
|
4410
|
-
const file = resolveWithinProject2(projectDir, compPath);
|
|
4411
|
-
let source = null;
|
|
4412
|
-
try {
|
|
4413
|
-
source = file ? readFileSync11(file, "utf-8") : null;
|
|
4414
|
-
} catch {
|
|
4415
|
-
}
|
|
4416
|
-
sources.set(compPath, source);
|
|
4417
|
-
}
|
|
4418
|
-
return sources.get(compPath) ?? null;
|
|
4419
|
-
};
|
|
4420
|
-
}
|
|
4421
|
-
function normalizeSource(src) {
|
|
4422
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) return null;
|
|
4423
|
-
const path = src.split(/[?#]/)[0]?.replace(/^\.?\//, "") ?? "";
|
|
4424
|
-
return path || null;
|
|
4425
|
-
}
|
|
4426
|
-
function closureOf(read, compPath) {
|
|
4427
|
-
const closure = /* @__PURE__ */ new Set();
|
|
4428
|
-
const visit = (path) => {
|
|
4429
|
-
if (closure.has(path)) return;
|
|
4430
|
-
closure.add(path);
|
|
4431
|
-
for (const match of (read(path) ?? "").matchAll(COMPOSITION_SRC)) {
|
|
4432
|
-
const mounted = normalizeSource(match[2] ?? "");
|
|
4433
|
-
if (mounted) visit(mounted);
|
|
4434
|
-
}
|
|
4435
|
-
};
|
|
4436
|
-
visit(compPath);
|
|
4437
|
-
return closure;
|
|
4438
|
-
}
|
|
4439
|
-
var inputSignatures = /* @__PURE__ */ new Map();
|
|
4440
|
-
function compositionInputSignature(projectDir, compPath, projectSignature) {
|
|
4441
|
-
const key = `${projectDir}\0${compPath}`;
|
|
4442
|
-
const known = inputSignatures.get(key);
|
|
4443
|
-
if (known?.projectSignature === projectSignature) return known.inputSignature;
|
|
4444
|
-
const read = projectReader(projectDir);
|
|
4445
|
-
const inputs = closureOf(read, normalizeSource(compPath) ?? compPath).add(ROOT_COMPOSITION);
|
|
4446
|
-
const siblings = [...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path));
|
|
4447
|
-
const inputSignature = createProjectSignature(projectDir, new Set(siblings));
|
|
4448
|
-
inputSignatures.set(key, { projectSignature, inputSignature });
|
|
4449
|
-
return inputSignature;
|
|
4450
|
-
}
|
|
4451
|
-
function compositionsAffectedBy(projectDir, changedPath) {
|
|
4452
|
-
const changed = changedPath.replace(/\\/g, "/");
|
|
4453
|
-
const read = projectReader(projectDir);
|
|
4454
|
-
const mounted = closureOf(read, ROOT_COMPOSITION);
|
|
4455
|
-
if (changed === ROOT_COMPOSITION || !mounted.has(changed)) return null;
|
|
4456
|
-
return [...mounted].filter((path) => closureOf(read, path).has(changed));
|
|
4457
|
-
}
|
|
4422
|
+
// src/routes/imageThumbnail.ts
|
|
4423
|
+
import { HTTPException } from "hono/http-exception";
|
|
4424
|
+
import { open, stat } from "fs/promises";
|
|
4425
|
+
import { createHash as createHash5 } from "crypto";
|
|
4426
|
+
import sharp from "sharp";
|
|
4458
4427
|
|
|
4459
4428
|
// src/routes/thumbnailGenerationCoordinator.ts
|
|
4460
4429
|
var ThumbnailGenerationCoordinator = class {
|
|
@@ -4473,10 +4442,10 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4473
4442
|
if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
4474
4443
|
let entry = this.entries.get(key);
|
|
4475
4444
|
if (!entry) {
|
|
4476
|
-
let
|
|
4445
|
+
let resolve9;
|
|
4477
4446
|
let reject;
|
|
4478
4447
|
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
4479
|
-
|
|
4448
|
+
resolve9 = resolvePromise;
|
|
4480
4449
|
reject = rejectPromise;
|
|
4481
4450
|
});
|
|
4482
4451
|
entry = {
|
|
@@ -4486,7 +4455,7 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4486
4455
|
state: "queued",
|
|
4487
4456
|
work,
|
|
4488
4457
|
promise,
|
|
4489
|
-
resolve:
|
|
4458
|
+
resolve: resolve9,
|
|
4490
4459
|
reject
|
|
4491
4460
|
};
|
|
4492
4461
|
this.entries.set(key, entry);
|
|
@@ -4500,7 +4469,7 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4500
4469
|
return /* @__PURE__ */ new Set([...this.entries.keys(), ...[...this.activeEntries].map((entry) => entry.key)]);
|
|
4501
4470
|
}
|
|
4502
4471
|
lease(entry, signal) {
|
|
4503
|
-
return new Promise((
|
|
4472
|
+
return new Promise((resolve9, reject) => {
|
|
4504
4473
|
let released = false;
|
|
4505
4474
|
const release = () => {
|
|
4506
4475
|
if (released) return;
|
|
@@ -4524,7 +4493,7 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4524
4493
|
entry.promise.then(
|
|
4525
4494
|
(value) => {
|
|
4526
4495
|
release();
|
|
4527
|
-
|
|
4496
|
+
resolve9(value);
|
|
4528
4497
|
},
|
|
4529
4498
|
(reason) => {
|
|
4530
4499
|
release();
|
|
@@ -4559,6 +4528,171 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4559
4528
|
};
|
|
4560
4529
|
var thumbnailGenerationCoordinator = new ThumbnailGenerationCoordinator(1);
|
|
4561
4530
|
|
|
4531
|
+
// src/routes/imageThumbnail.ts
|
|
4532
|
+
var MAX_SOURCE_BYTES = 64 * 1024 * 1024;
|
|
4533
|
+
var MAX_CACHE_BYTES = 8 * 1024 * 1024;
|
|
4534
|
+
async function generateThumbnail(path, signal) {
|
|
4535
|
+
const file = await open(path, "r");
|
|
4536
|
+
try {
|
|
4537
|
+
const info = await file.stat();
|
|
4538
|
+
if (!info.isFile() || info.size > MAX_SOURCE_BYTES) throw new Error("Unsupported image");
|
|
4539
|
+
const source = Buffer.alloc(info.size);
|
|
4540
|
+
let offset = 0;
|
|
4541
|
+
while (offset < source.length) {
|
|
4542
|
+
signal.throwIfAborted();
|
|
4543
|
+
const { bytesRead } = await file.read(source, offset, source.length - offset, offset);
|
|
4544
|
+
if (!bytesRead) break;
|
|
4545
|
+
offset += bytesRead;
|
|
4546
|
+
}
|
|
4547
|
+
if (source[0] !== 255 || source[1] !== 216) throw new Error("Unsupported image");
|
|
4548
|
+
const image = sharp(source.subarray(0, offset), { limitInputPixels: 4e7 });
|
|
4549
|
+
if ((await image.metadata()).format !== "jpeg") throw new Error("Unsupported image");
|
|
4550
|
+
signal.throwIfAborted();
|
|
4551
|
+
const result = await image.rotate().resize(240, 135, { fit: "inside", withoutEnlargement: true }).jpeg({ quality: 80 }).toBuffer();
|
|
4552
|
+
signal.throwIfAborted();
|
|
4553
|
+
return result;
|
|
4554
|
+
} finally {
|
|
4555
|
+
await file.close();
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
async function resolveSource(adapter, id, url) {
|
|
4559
|
+
const project = await adapter.resolveProject(id);
|
|
4560
|
+
if (!project) throw new HTTPException(404);
|
|
4561
|
+
let relative6;
|
|
4562
|
+
try {
|
|
4563
|
+
relative6 = requestSubPath(url, "projects/:id/image-thumbnail");
|
|
4564
|
+
} catch {
|
|
4565
|
+
throw new HTTPException(400);
|
|
4566
|
+
}
|
|
4567
|
+
if (!/\.jpe?g$/i.test(relative6)) throw new HTTPException(415);
|
|
4568
|
+
const path = pinWithinProject(project.dir, relative6);
|
|
4569
|
+
if (!path) throw new HTTPException(404);
|
|
4570
|
+
const info = await stat(path).catch(() => {
|
|
4571
|
+
throw new HTTPException(404);
|
|
4572
|
+
});
|
|
4573
|
+
if (!info.isFile()) throw new HTTPException(404);
|
|
4574
|
+
if (info.size > MAX_SOURCE_BYTES) throw new HTTPException(413);
|
|
4575
|
+
return { path, info };
|
|
4576
|
+
}
|
|
4577
|
+
function registerImageThumbnailRoutes(api, adapter) {
|
|
4578
|
+
const coordinator = new ThumbnailGenerationCoordinator(2);
|
|
4579
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4580
|
+
let cacheBytes = 0;
|
|
4581
|
+
const remember = (key, image) => {
|
|
4582
|
+
cacheBytes -= cache.get(key)?.byteLength ?? 0;
|
|
4583
|
+
cache.delete(key);
|
|
4584
|
+
cache.set(key, image);
|
|
4585
|
+
cacheBytes += image.byteLength;
|
|
4586
|
+
while (cacheBytes > MAX_CACHE_BYTES || cache.size > 128) {
|
|
4587
|
+
const oldest = cache.keys().next().value;
|
|
4588
|
+
if (oldest === void 0) break;
|
|
4589
|
+
cacheBytes -= cache.get(oldest)?.byteLength ?? 0;
|
|
4590
|
+
cache.delete(oldest);
|
|
4591
|
+
}
|
|
4592
|
+
};
|
|
4593
|
+
api.get("/projects/:id/image-thumbnail/*", async (c) => {
|
|
4594
|
+
try {
|
|
4595
|
+
const { path, info } = await resolveSource(adapter, c.req.param("id"), c.req.url);
|
|
4596
|
+
const key = createHash5("sha256").update(JSON.stringify([path, info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs])).digest("hex");
|
|
4597
|
+
const etag = `"${key}"`;
|
|
4598
|
+
c.header("ETag", etag);
|
|
4599
|
+
c.header("Cache-Control", "private, no-cache");
|
|
4600
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
4601
|
+
if (c.req.header("If-None-Match") === etag) return c.body(null, 304);
|
|
4602
|
+
let result = cache.get(key);
|
|
4603
|
+
if (result) remember(key, result);
|
|
4604
|
+
else {
|
|
4605
|
+
if (coordinator.protectedKeys().size >= 32) return c.text("thumbnail queue full", 429);
|
|
4606
|
+
result = await coordinator.acquire(key, c.req.raw.signal, async (signal) => {
|
|
4607
|
+
const image = await generateThumbnail(path, signal);
|
|
4608
|
+
remember(key, image);
|
|
4609
|
+
return image;
|
|
4610
|
+
}) ?? void 0;
|
|
4611
|
+
}
|
|
4612
|
+
if (!result) return c.text("image unavailable", 422);
|
|
4613
|
+
c.header("Content-Type", "image/jpeg");
|
|
4614
|
+
return c.body(new Uint8Array(result));
|
|
4615
|
+
} catch (error) {
|
|
4616
|
+
if (error instanceof HTTPException) return error.getResponse();
|
|
4617
|
+
return c.text("image unavailable", 422);
|
|
4618
|
+
}
|
|
4619
|
+
});
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4622
|
+
// src/routes/thumbnail.ts
|
|
4623
|
+
import {
|
|
4624
|
+
closeSync as closeSync3,
|
|
4625
|
+
fstatSync as fstatSync3,
|
|
4626
|
+
openSync as openSync3,
|
|
4627
|
+
mkdirSync as mkdirSync6,
|
|
4628
|
+
readFileSync as readFileSync12,
|
|
4629
|
+
readdirSync as readdirSync5,
|
|
4630
|
+
renameSync as renameSync3,
|
|
4631
|
+
rmSync as rmSync4,
|
|
4632
|
+
statSync as statSync6,
|
|
4633
|
+
unlinkSync as unlinkSync4,
|
|
4634
|
+
writeFileSync as writeFileSync6
|
|
4635
|
+
} from "fs";
|
|
4636
|
+
import { join as join12 } from "path";
|
|
4637
|
+
import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
|
|
4638
|
+
|
|
4639
|
+
// src/helpers/compositionInputs.ts
|
|
4640
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
4641
|
+
var ROOT_COMPOSITION = "index.html";
|
|
4642
|
+
var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
|
|
4643
|
+
function projectReader(projectDir) {
|
|
4644
|
+
const sources = /* @__PURE__ */ new Map();
|
|
4645
|
+
return (compPath) => {
|
|
4646
|
+
if (!sources.has(compPath)) {
|
|
4647
|
+
const file = resolveWithinProject(projectDir, compPath);
|
|
4648
|
+
let source = null;
|
|
4649
|
+
try {
|
|
4650
|
+
source = file ? readFileSync11(file, "utf-8") : null;
|
|
4651
|
+
} catch {
|
|
4652
|
+
}
|
|
4653
|
+
sources.set(compPath, source);
|
|
4654
|
+
}
|
|
4655
|
+
return sources.get(compPath) ?? null;
|
|
4656
|
+
};
|
|
4657
|
+
}
|
|
4658
|
+
function normalizeSource(src) {
|
|
4659
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) return null;
|
|
4660
|
+
const path = src.split(/[?#]/)[0]?.replace(/^\.?\//, "") ?? "";
|
|
4661
|
+
return path || null;
|
|
4662
|
+
}
|
|
4663
|
+
function closureOf(read, compPath) {
|
|
4664
|
+
const closure = /* @__PURE__ */ new Set();
|
|
4665
|
+
const visit = (path) => {
|
|
4666
|
+
if (closure.has(path)) return;
|
|
4667
|
+
closure.add(path);
|
|
4668
|
+
for (const match of (read(path) ?? "").matchAll(COMPOSITION_SRC)) {
|
|
4669
|
+
const mounted = normalizeSource(match[2] ?? "");
|
|
4670
|
+
if (mounted) visit(mounted);
|
|
4671
|
+
}
|
|
4672
|
+
};
|
|
4673
|
+
visit(compPath);
|
|
4674
|
+
return closure;
|
|
4675
|
+
}
|
|
4676
|
+
var inputSignatures = /* @__PURE__ */ new Map();
|
|
4677
|
+
function compositionInputSignature(projectDir, compPath, projectSignature) {
|
|
4678
|
+
const key = `${projectDir}\0${compPath}`;
|
|
4679
|
+
const known = inputSignatures.get(key);
|
|
4680
|
+
if (known?.projectSignature === projectSignature) return known.inputSignature;
|
|
4681
|
+
const read = projectReader(projectDir);
|
|
4682
|
+
const inputs = closureOf(read, normalizeSource(compPath) ?? compPath).add(ROOT_COMPOSITION);
|
|
4683
|
+
const siblings = [...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path));
|
|
4684
|
+
const inputSignature = createProjectSignature(projectDir, new Set(siblings));
|
|
4685
|
+
inputSignatures.set(key, { projectSignature, inputSignature });
|
|
4686
|
+
return inputSignature;
|
|
4687
|
+
}
|
|
4688
|
+
function compositionsAffectedBy(projectDir, changedPath) {
|
|
4689
|
+
const changed = changedPath.replace(/\\/g, "/");
|
|
4690
|
+
const read = projectReader(projectDir);
|
|
4691
|
+
const mounted = closureOf(read, ROOT_COMPOSITION);
|
|
4692
|
+
if (changed === ROOT_COMPOSITION || !mounted.has(changed)) return null;
|
|
4693
|
+
return [...mounted].filter((path) => closureOf(read, path).has(changed));
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4562
4696
|
// src/routes/thumbnail.ts
|
|
4563
4697
|
var THUMBNAIL_CACHE_VERSION = "v4";
|
|
4564
4698
|
var THUMBNAIL_MAX_OUTPUT_WIDTH = 240;
|
|
@@ -4567,8 +4701,14 @@ var THUMBNAIL_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
|
4567
4701
|
var THUMBNAIL_CACHE_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
4568
4702
|
var prunedCacheDirs = /* @__PURE__ */ new Set();
|
|
4569
4703
|
function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
|
|
4570
|
-
|
|
4571
|
-
|
|
4704
|
+
let entries;
|
|
4705
|
+
try {
|
|
4706
|
+
entries = readdirSync5(cacheDir, { withFileTypes: true });
|
|
4707
|
+
} catch (err) {
|
|
4708
|
+
if (err.code === "ENOENT") return;
|
|
4709
|
+
throw err;
|
|
4710
|
+
}
|
|
4711
|
+
const files = entries.flatMap((entry) => {
|
|
4572
4712
|
if (!entry.isFile()) return [];
|
|
4573
4713
|
const path = join12(cacheDir, entry.name);
|
|
4574
4714
|
try {
|
|
@@ -4606,22 +4746,30 @@ function writeThumbnailAtomically(path, buffer) {
|
|
|
4606
4746
|
rmSync4(temporaryPath, { force: true });
|
|
4607
4747
|
}
|
|
4608
4748
|
}
|
|
4609
|
-
function
|
|
4749
|
+
function readFileOnce(file) {
|
|
4610
4750
|
let fd;
|
|
4611
4751
|
try {
|
|
4612
4752
|
fd = openSync3(file, "r");
|
|
4613
4753
|
} catch (err) {
|
|
4614
|
-
|
|
4754
|
+
const code = err.code;
|
|
4755
|
+
if (code === "ENOENT") return "missing";
|
|
4756
|
+
if (code === "ENOTDIR") return "not-a-file";
|
|
4615
4757
|
throw err;
|
|
4616
4758
|
}
|
|
4617
4759
|
try {
|
|
4618
|
-
const
|
|
4619
|
-
if (!
|
|
4620
|
-
return {
|
|
4760
|
+
const stat3 = fstatSync3(fd);
|
|
4761
|
+
if (!stat3.isFile()) return "not-a-file";
|
|
4762
|
+
return { data: readFileSync12(fd), mtimeMs: stat3.mtimeMs };
|
|
4621
4763
|
} finally {
|
|
4622
4764
|
closeSync3(fd);
|
|
4623
4765
|
}
|
|
4624
4766
|
}
|
|
4767
|
+
function manifestKey(file) {
|
|
4768
|
+
const manifest = readFileOnce(file);
|
|
4769
|
+
if (typeof manifest === "string") return { key: "", mtimeMs: -Infinity };
|
|
4770
|
+
const hash = createHash6("sha1").update(manifest.data).digest("hex").slice(0, 16);
|
|
4771
|
+
return { key: `_${hash}`, mtimeMs: Math.round(manifest.mtimeMs) };
|
|
4772
|
+
}
|
|
4625
4773
|
function registerThumbnailRoutes(api, adapter) {
|
|
4626
4774
|
api.get("/projects/:id/thumbnail/*", async (c) => {
|
|
4627
4775
|
if (!adapter.generateThumbnail) {
|
|
@@ -4632,8 +4780,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4632
4780
|
const { project, signature: projectSignature } = resolved;
|
|
4633
4781
|
let compPath = requestSubPath(c.req.url, "projects/:id/thumbnail");
|
|
4634
4782
|
if (compPath && !compPath.includes(".")) compPath += ".html";
|
|
4635
|
-
const htmlFile =
|
|
4636
|
-
const source = htmlFile ?
|
|
4783
|
+
const htmlFile = resolveWithinProject(project.dir, compPath);
|
|
4784
|
+
const source = htmlFile ? readFileOnce(htmlFile) : "not-a-file";
|
|
4637
4785
|
if (source === "not-a-file") return c.json({ error: "not found" }, 404);
|
|
4638
4786
|
const inputSignature = compositionInputSignature(project.dir, compPath, projectSignature);
|
|
4639
4787
|
const url = new URL(c.req.url, `http://${c.req.header("host") || "localhost"}`);
|
|
@@ -4655,8 +4803,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4655
4803
|
let sourceMtime = 0;
|
|
4656
4804
|
let sourceKey = "";
|
|
4657
4805
|
if (source !== "missing") {
|
|
4658
|
-
const
|
|
4659
|
-
sourceKey = `_${
|
|
4806
|
+
const html = source.data.toString("utf-8");
|
|
4807
|
+
sourceKey = `_${createHash6("sha1").update(html).digest("hex").slice(0, 16)}`;
|
|
4660
4808
|
sourceMtime = Math.round(source.mtimeMs);
|
|
4661
4809
|
if (!vpWidth) {
|
|
4662
4810
|
const wMatch = html.match(/data-width=["'](\d+)["']/);
|
|
@@ -4665,30 +4813,19 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4665
4813
|
if (hMatch?.[1]) compH = parseInt(hMatch[1]);
|
|
4666
4814
|
}
|
|
4667
4815
|
}
|
|
4668
|
-
const
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
const manualEditsContent = readFileSync12(manualEditsFile, "utf-8");
|
|
4672
|
-
manualEditsKey = `_${createHash5("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
|
|
4673
|
-
sourceMtime = Math.max(sourceMtime, Math.round(statSync6(manualEditsFile).mtimeMs));
|
|
4674
|
-
}
|
|
4675
|
-
const motionFile = join12(project.dir, STUDIO_MOTION_PATH);
|
|
4676
|
-
let motionKey = "";
|
|
4677
|
-
if (existsSync7(motionFile)) {
|
|
4678
|
-
const motionContent = readFileSync12(motionFile, "utf-8");
|
|
4679
|
-
motionKey = `_${createHash5("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
|
|
4680
|
-
sourceMtime = Math.max(sourceMtime, Math.round(statSync6(motionFile).mtimeMs));
|
|
4681
|
-
}
|
|
4816
|
+
const manualEdits = manifestKey(join12(project.dir, STUDIO_MANUAL_EDITS_PATH));
|
|
4817
|
+
const motion = manifestKey(join12(project.dir, STUDIO_MOTION_PATH));
|
|
4818
|
+
sourceMtime = Math.max(sourceMtime, manualEdits.mtimeMs, motion.mtimeMs);
|
|
4682
4819
|
const projectUrl = `http://${c.req.header("host")}/api/projects/${encodeURIComponent(project.id)}`;
|
|
4683
4820
|
const previewUrl = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
|
|
4684
4821
|
const cacheDir = join12(project.dir, ".thumbnails");
|
|
4685
4822
|
const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
|
|
4686
4823
|
const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
|
|
4687
|
-
const inputSignatureKey = `_${
|
|
4824
|
+
const inputSignatureKey = `_${createHash6("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
|
|
4688
4825
|
const outputScale = outputMode === "source" ? 1 : Math.min(1, THUMBNAIL_MAX_OUTPUT_WIDTH / compW, THUMBNAIL_MAX_OUTPUT_HEIGHT / compH);
|
|
4689
4826
|
const outputWidth = Math.max(1, Math.round(compW * outputScale));
|
|
4690
4827
|
const outputHeight = Math.max(1, Math.round(compH * outputScale));
|
|
4691
|
-
const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${inputSignatureKey}${
|
|
4828
|
+
const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${inputSignatureKey}${manualEdits.key}${motion.key}${sourceKey}_${format}_${outputMode}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${outputWidth}x${outputHeight}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
|
|
4692
4829
|
const cachePath = join12(cacheDir, cacheKey);
|
|
4693
4830
|
if (!prunedCacheDirs.has(cacheDir)) {
|
|
4694
4831
|
prunedCacheDirs.add(cacheDir);
|
|
@@ -4697,8 +4834,9 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4697
4834
|
/* @__PURE__ */ new Set([...thumbnailGenerationCoordinator.protectedKeys(), cachePath])
|
|
4698
4835
|
);
|
|
4699
4836
|
}
|
|
4700
|
-
|
|
4701
|
-
|
|
4837
|
+
const cached = readFileOnce(cachePath);
|
|
4838
|
+
if (typeof cached === "object") {
|
|
4839
|
+
return new Response(new Uint8Array(cached.data), {
|
|
4702
4840
|
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
4703
4841
|
});
|
|
4704
4842
|
}
|
|
@@ -4728,7 +4866,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4728
4866
|
if (afterGeneration?.project.dir !== project.dir || !inputsUnchanged(afterGeneration.signature) || !inputsUnchanged(createProjectSignature(project.dir))) {
|
|
4729
4867
|
return generated;
|
|
4730
4868
|
}
|
|
4731
|
-
|
|
4869
|
+
mkdirSync6(cacheDir, { recursive: true });
|
|
4732
4870
|
writeThumbnailAtomically(cachePath, generated);
|
|
4733
4871
|
return generated;
|
|
4734
4872
|
}
|
|
@@ -4754,15 +4892,15 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4754
4892
|
}
|
|
4755
4893
|
|
|
4756
4894
|
// src/routes/waveform.ts
|
|
4757
|
-
import { existsSync as
|
|
4758
|
-
import { join as join13, resolve as
|
|
4895
|
+
import { existsSync as existsSync7, readFileSync as readFileSync13, statSync as statSync7 } from "fs";
|
|
4896
|
+
import { join as join13, resolve as resolve6 } from "path";
|
|
4759
4897
|
import { isWithinProjectRoot as isWithinProjectRoot2 } from "@hyperframes/parsers/asset-resolution";
|
|
4760
4898
|
function registerWaveformRoutes(api, adapter) {
|
|
4761
4899
|
api.get("/projects/:id/waveform/*", async (c) => {
|
|
4762
4900
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
4763
4901
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
4764
4902
|
const assetPath = requestSubPath(c.req.url, "projects/:id/waveform");
|
|
4765
|
-
const audioPath =
|
|
4903
|
+
const audioPath = resolve6(project.dir, assetPath);
|
|
4766
4904
|
if (!isWithinProjectRoot2(project.dir, audioPath)) {
|
|
4767
4905
|
return c.json({ error: "file not found" }, 404);
|
|
4768
4906
|
}
|
|
@@ -4771,7 +4909,7 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4771
4909
|
const cacheDir = join13(project.dir, ".waveform-cache");
|
|
4772
4910
|
const cachePath = join13(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
4773
4911
|
try {
|
|
4774
|
-
if (isWaveformCacheDirectory(cacheDir) &&
|
|
4912
|
+
if (isWaveformCacheDirectory(cacheDir) && existsSync7(cachePath)) {
|
|
4775
4913
|
const peaks2 = JSON.parse(readFileSync13(cachePath, "utf-8"));
|
|
4776
4914
|
return c.json({ peaks: peaks2 });
|
|
4777
4915
|
}
|
|
@@ -4904,12 +5042,12 @@ function registerFontRoutes(api) {
|
|
|
4904
5042
|
return c.json({ error: "font file not accessible" }, 404);
|
|
4905
5043
|
}
|
|
4906
5044
|
try {
|
|
4907
|
-
const
|
|
4908
|
-
if (
|
|
5045
|
+
const stat3 = fstatSync4(fd);
|
|
5046
|
+
if (stat3.size > SYSTEM_FONT_SIZE_LIMIT) {
|
|
4909
5047
|
return c.json({ error: "font file too large" }, 413);
|
|
4910
5048
|
}
|
|
4911
|
-
const buffer = Buffer.alloc(
|
|
4912
|
-
readSync(fd, buffer, 0,
|
|
5049
|
+
const buffer = Buffer.alloc(stat3.size);
|
|
5050
|
+
readSync(fd, buffer, 0, stat3.size, 0);
|
|
4913
5051
|
const mimeType = located.format === "otf" ? "font/otf" : located.format === "woff2" ? "font/woff2" : located.format === "woff" ? "font/woff" : located.format === "ttc" ? "font/collection" : "font/ttf";
|
|
4914
5052
|
const fileName = `${family.replace(/[^a-zA-Z0-9 -]/g, "")}.${located.format}`;
|
|
4915
5053
|
return new Response(buffer, {
|
|
@@ -4937,7 +5075,7 @@ function registerRegistryRoutes(api, adapter) {
|
|
|
4937
5075
|
});
|
|
4938
5076
|
api.post("/projects/:id/registry/install", async (c) => {
|
|
4939
5077
|
if (!adapter.installRegistryBlock) {
|
|
4940
|
-
return c.json({ error: "
|
|
5078
|
+
return c.json({ error: "Installing catalog items needs hyperframes preview" }, 501);
|
|
4941
5079
|
}
|
|
4942
5080
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
4943
5081
|
if (!project) return c.json({ error: "Project not found" }, 404);
|
|
@@ -5055,8 +5193,8 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
5055
5193
|
|
|
5056
5194
|
// src/routes/media.ts
|
|
5057
5195
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
5058
|
-
import { existsSync as
|
|
5059
|
-
import { basename as
|
|
5196
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
|
|
5197
|
+
import { basename as basename2, dirname as dirname6, extname as extname2, join as join14 } from "path";
|
|
5060
5198
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5061
5199
|
".mp4",
|
|
5062
5200
|
".mov",
|
|
@@ -5086,14 +5224,14 @@ function containsNullByte(path) {
|
|
|
5086
5224
|
return path.includes("\0");
|
|
5087
5225
|
}
|
|
5088
5226
|
function slugFileBase(path) {
|
|
5089
|
-
const name =
|
|
5227
|
+
const name = basename2(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5090
5228
|
return name || "media";
|
|
5091
5229
|
}
|
|
5092
5230
|
function uniqueAssetPath(projectDir, assetPath) {
|
|
5093
5231
|
const ext = extname2(assetPath);
|
|
5094
5232
|
const withoutExt = assetPath.slice(0, -ext.length);
|
|
5095
5233
|
let candidate = assetPath;
|
|
5096
|
-
for (let index = 2;
|
|
5234
|
+
for (let index = 2; existsSync8(join14(projectDir, candidate)); index++) {
|
|
5097
5235
|
candidate = `${withoutExt}-${index}${ext}`;
|
|
5098
5236
|
}
|
|
5099
5237
|
return candidate;
|
|
@@ -5142,9 +5280,9 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5142
5280
|
if (/^(?:https?:|data:|blob:)/i.test(assetPath)) {
|
|
5143
5281
|
return c.json({ error: "media metadata requires a project-local asset" }, 400);
|
|
5144
5282
|
}
|
|
5145
|
-
const filePath =
|
|
5283
|
+
const filePath = resolveWithinProject(project.dir, assetPath);
|
|
5146
5284
|
if (!filePath) return c.json({ error: "forbidden" }, 403);
|
|
5147
|
-
if (!
|
|
5285
|
+
if (!existsSync8(filePath)) return c.json({ error: "media not found" }, 404);
|
|
5148
5286
|
return c.json({ path: assetPath, metadata: await readMediaMetadata(filePath) });
|
|
5149
5287
|
});
|
|
5150
5288
|
api.post(
|
|
@@ -5164,9 +5302,9 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5164
5302
|
if (/^(?:https?:|data:|blob:)/i.test(inputAssetPath)) {
|
|
5165
5303
|
return c.json({ error: "background removal requires a project-local media asset" }, 400);
|
|
5166
5304
|
}
|
|
5167
|
-
const inputPath =
|
|
5305
|
+
const inputPath = resolveWithinProject(project.dir, inputAssetPath);
|
|
5168
5306
|
if (!inputPath) return c.json({ error: "forbidden" }, 403);
|
|
5169
|
-
if (!
|
|
5307
|
+
if (!existsSync8(inputPath)) return c.json({ error: "input media not found" }, 404);
|
|
5170
5308
|
const inputIsVideo = isVideoPath(inputAssetPath);
|
|
5171
5309
|
const inputIsImage = isImagePath(inputAssetPath);
|
|
5172
5310
|
if (!inputIsVideo && !inputIsImage) {
|
|
@@ -5176,11 +5314,11 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5176
5314
|
if (requestedOutput && containsNullByte(requestedOutput)) {
|
|
5177
5315
|
return c.json({ error: "forbidden" }, 403);
|
|
5178
5316
|
}
|
|
5179
|
-
if (requestedOutput && !
|
|
5317
|
+
if (requestedOutput && !resolveWithinProject(project.dir, requestedOutput)) {
|
|
5180
5318
|
return c.json({ error: "forbidden" }, 403);
|
|
5181
5319
|
}
|
|
5182
5320
|
const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput) : defaultOutputPath(project.dir, inputAssetPath);
|
|
5183
|
-
const outputPath =
|
|
5321
|
+
const outputPath = resolveWithinProject(project.dir, outputAssetPath);
|
|
5184
5322
|
if (!outputPath) return c.json({ error: "forbidden" }, 403);
|
|
5185
5323
|
if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname2(outputAssetPath).toLowerCase())) {
|
|
5186
5324
|
return c.json({ error: "video background removal output must be .webm or .mov" }, 400);
|
|
@@ -5195,7 +5333,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5195
5333
|
return c.json({ error: "background plates are only supported for video inputs" }, 400);
|
|
5196
5334
|
}
|
|
5197
5335
|
backgroundOutputAssetPath = defaultPlatePath(project.dir, inputAssetPath);
|
|
5198
|
-
backgroundOutputPath =
|
|
5336
|
+
backgroundOutputPath = resolveWithinProject(project.dir, backgroundOutputAssetPath) ?? void 0;
|
|
5199
5337
|
if (!backgroundOutputPath) {
|
|
5200
5338
|
return c.json({ error: "forbidden" }, 403);
|
|
5201
5339
|
}
|
|
@@ -5258,12 +5396,12 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5258
5396
|
}
|
|
5259
5397
|
|
|
5260
5398
|
// src/routes/globalAssets.ts
|
|
5261
|
-
import { existsSync as
|
|
5399
|
+
import { existsSync as existsSync9, readFileSync as readFileSync14 } from "fs";
|
|
5262
5400
|
import { homedir } from "os";
|
|
5263
5401
|
import { join as join15 } from "path";
|
|
5264
5402
|
function readGlobalAssets(home = homedir()) {
|
|
5265
5403
|
const manifestPath = join15(home, ".media", "manifest.jsonl");
|
|
5266
|
-
if (!
|
|
5404
|
+
if (!existsSync9(manifestPath)) return [];
|
|
5267
5405
|
const out = [];
|
|
5268
5406
|
for (const line of readFileSync14(manifestPath, "utf8").split("\n")) {
|
|
5269
5407
|
if (!line.trim()) continue;
|
|
@@ -5286,26 +5424,26 @@ function registerGlobalAssetRoutes(api) {
|
|
|
5286
5424
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
5287
5425
|
import { readdir as readdir2, rm as rm2, rmdir, writeFile } from "fs/promises";
|
|
5288
5426
|
import {
|
|
5289
|
-
existsSync as
|
|
5427
|
+
existsSync as existsSync11,
|
|
5290
5428
|
lstatSync as lstatSync4,
|
|
5291
5429
|
statSync as statSync9,
|
|
5292
5430
|
mkdirSync as mkdirSync11,
|
|
5293
|
-
readdirSync as
|
|
5431
|
+
readdirSync as readdirSync6,
|
|
5294
5432
|
readFileSync as readFileSync18,
|
|
5295
|
-
realpathSync
|
|
5433
|
+
realpathSync,
|
|
5296
5434
|
rmSync as rmSync6
|
|
5297
5435
|
} from "fs";
|
|
5298
5436
|
import { homedir as homedir2 } from "os";
|
|
5299
|
-
import { dirname as dirname9, isAbsolute as
|
|
5437
|
+
import { dirname as dirname9, isAbsolute as isAbsolute2, join as join19, relative as relative5, resolve as resolve8, sep as sep3 } from "path";
|
|
5300
5438
|
|
|
5301
5439
|
// src/history/blobStore.ts
|
|
5302
|
-
import { createHash as
|
|
5440
|
+
import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
|
|
5303
5441
|
import { constants as constants3, createReadStream as createReadStream2, renameSync as renameSync4 } from "fs";
|
|
5304
|
-
import { copyFile, mkdir, readdir, readFile as readFile2, rename, rm, stat } from "fs/promises";
|
|
5442
|
+
import { copyFile, mkdir, readdir, readFile as readFile2, rename, rm, stat as stat2 } from "fs/promises";
|
|
5305
5443
|
import { dirname as dirname7, join as join16 } from "path";
|
|
5306
5444
|
var BLOB_HASH = /^[0-9a-f]{64}$/;
|
|
5307
5445
|
async function hashFile(path) {
|
|
5308
|
-
const hash =
|
|
5446
|
+
const hash = createHash7("sha256");
|
|
5309
5447
|
for await (const chunk of createReadStream2(path)) hash.update(chunk);
|
|
5310
5448
|
return hash.digest("hex");
|
|
5311
5449
|
}
|
|
@@ -5326,7 +5464,7 @@ async function openBlobStore(dir) {
|
|
|
5326
5464
|
await mkdir(dir, { recursive: true });
|
|
5327
5465
|
for (const shard of await readdir(dir))
|
|
5328
5466
|
for (const hash of await readdir(join16(dir, shard)).catch(() => []))
|
|
5329
|
-
sizes.set(hash, (await
|
|
5467
|
+
sizes.set(hash, (await stat2(join16(dir, shard, hash))).size);
|
|
5330
5468
|
const pathOf = (hash) => {
|
|
5331
5469
|
if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
|
|
5332
5470
|
return join16(dir, hash.slice(0, 2), hash);
|
|
@@ -5343,7 +5481,7 @@ async function openBlobStore(dir) {
|
|
|
5343
5481
|
return hash;
|
|
5344
5482
|
}
|
|
5345
5483
|
await mkdir(dirname7(pathOf(hash)), { recursive: true });
|
|
5346
|
-
const size = (await
|
|
5484
|
+
const size = (await stat2(temp)).size;
|
|
5347
5485
|
await rename(temp, pathOf(hash));
|
|
5348
5486
|
sizes.set(hash, size);
|
|
5349
5487
|
total += size;
|
|
@@ -5367,8 +5505,8 @@ async function openBlobStore(dir) {
|
|
|
5367
5505
|
|
|
5368
5506
|
// src/history/historyId.ts
|
|
5369
5507
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
5370
|
-
import { existsSync as
|
|
5371
|
-
import { join as join17, resolve as
|
|
5508
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync15, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
5509
|
+
import { basename as basename3, join as join17, resolve as resolve7 } from "path";
|
|
5372
5510
|
var ID_PATH = join17(".hyperframes", "history-id");
|
|
5373
5511
|
var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
5374
5512
|
function readId(projectDir) {
|
|
@@ -5381,18 +5519,38 @@ function readId(projectDir) {
|
|
|
5381
5519
|
}
|
|
5382
5520
|
var sameFolder = (a, b) => a.ino === b.ino && a.birthtimeMs === b.birthtimeMs;
|
|
5383
5521
|
function isRecordedFolder(historyDir, folder) {
|
|
5522
|
+
const was = readRecord(historyDir);
|
|
5523
|
+
if (typeof was?.ino === "number")
|
|
5524
|
+
return sameFolder({ ino: was.ino, birthtimeMs: was.born ?? NaN }, folder);
|
|
5525
|
+
const unreadableSo0878KeptTheId = typeof was?.dir !== "string";
|
|
5526
|
+
if (unreadableSo0878KeptTheId) return true;
|
|
5527
|
+
return !isCopyOf0878Folder(was.dir, basename3(historyDir), folder);
|
|
5528
|
+
}
|
|
5529
|
+
function readRecord(historyDir) {
|
|
5530
|
+
try {
|
|
5531
|
+
return JSON.parse(readFileSync15(join17(historyDir, "project.json"), "utf-8"));
|
|
5532
|
+
} catch {
|
|
5533
|
+
return null;
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
function isCopyOf0878Folder(recordedDir, id, folder) {
|
|
5537
|
+
let there;
|
|
5384
5538
|
try {
|
|
5385
|
-
|
|
5386
|
-
return sameFolder({ ino: was.ino, birthtimeMs: was.born }, folder);
|
|
5539
|
+
there = statSync8(recordedDir, { throwIfNoEntry: false });
|
|
5387
5540
|
} catch {
|
|
5388
5541
|
return false;
|
|
5389
5542
|
}
|
|
5543
|
+
return !!there && !sameFolder(there, folder) && readId(recordedDir) === id;
|
|
5390
5544
|
}
|
|
5391
5545
|
function projectHistoryId(projectDir, historyRoot) {
|
|
5392
|
-
const dir =
|
|
5546
|
+
const dir = resolve7(projectDir);
|
|
5393
5547
|
const folder = statSync8(dir);
|
|
5394
5548
|
let id = readId(dir);
|
|
5395
|
-
if (!id
|
|
5549
|
+
if (!id && existsSync10(join17(dir, ID_PATH)))
|
|
5550
|
+
throw new HistoryIdError(
|
|
5551
|
+
`${join17(dir, ID_PATH)} holds no history id this version can read; move it aside to start anew.`
|
|
5552
|
+
);
|
|
5553
|
+
if (!id || existsSync10(join17(historyRoot, id)) && !isRecordedFolder(join17(historyRoot, id), folder)) {
|
|
5396
5554
|
id = randomUUID6();
|
|
5397
5555
|
mkdirSync8(join17(dir, ".hyperframes"), { recursive: true });
|
|
5398
5556
|
writeFileSync7(join17(dir, ID_PATH), `${id}
|
|
@@ -5401,6 +5559,12 @@ function projectHistoryId(projectDir, historyRoot) {
|
|
|
5401
5559
|
recordProject(join17(historyRoot, id), dir, folder);
|
|
5402
5560
|
return id;
|
|
5403
5561
|
}
|
|
5562
|
+
var HistoryIdError = class extends Error {
|
|
5563
|
+
constructor(message) {
|
|
5564
|
+
super(message);
|
|
5565
|
+
this.name = "HistoryIdError";
|
|
5566
|
+
}
|
|
5567
|
+
};
|
|
5404
5568
|
function recordProject(historyDir, dir, folder) {
|
|
5405
5569
|
const record = { dir, ino: folder.ino, born: folder.birthtimeMs };
|
|
5406
5570
|
mkdirSync8(historyDir, { recursive: true });
|
|
@@ -5645,7 +5809,7 @@ async function removeEmptyFolders(dir) {
|
|
|
5645
5809
|
await rmdir(dir);
|
|
5646
5810
|
}
|
|
5647
5811
|
function fileLeftIn(dir, at, deleted) {
|
|
5648
|
-
for (const entry of
|
|
5812
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
5649
5813
|
const path = `${at}/${entry.name}`;
|
|
5650
5814
|
const left = entry.isDirectory() ? fileLeftIn(join19(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
|
|
5651
5815
|
if (left) return left;
|
|
@@ -5670,7 +5834,7 @@ var Engine = class {
|
|
|
5670
5834
|
this.options = options;
|
|
5671
5835
|
this.projectId = projectId;
|
|
5672
5836
|
this.blobs = blobs;
|
|
5673
|
-
this.dir =
|
|
5837
|
+
this.dir = resolve8(options.projectDir);
|
|
5674
5838
|
this.home = join19(options.historyRoot, projectId);
|
|
5675
5839
|
this.foldsCase = ignoresCase(this.dir);
|
|
5676
5840
|
const folder = statSync9(this.dir, { throwIfNoEntry: false });
|
|
@@ -5732,7 +5896,7 @@ var Engine = class {
|
|
|
5732
5896
|
persistLog(record) {
|
|
5733
5897
|
if (record) saveRecord(this.logFile, this.log, record);
|
|
5734
5898
|
else writeLog(this.logFile, this.log);
|
|
5735
|
-
if (!
|
|
5899
|
+
if (!existsSync11(join19(this.home, "project.json")))
|
|
5736
5900
|
recordProject(this.home, this.dir, this.folder);
|
|
5737
5901
|
}
|
|
5738
5902
|
replaced() {
|
|
@@ -5837,17 +6001,17 @@ var Engine = class {
|
|
|
5837
6001
|
throw error;
|
|
5838
6002
|
}
|
|
5839
6003
|
}
|
|
5840
|
-
async observe(path,
|
|
6004
|
+
async observe(path, stat3, at) {
|
|
5841
6005
|
const known = this.tracked.get(path) ?? { hash: null, stat: null };
|
|
5842
|
-
if (
|
|
6006
|
+
if (stat3 && known.stat === stat3) return false;
|
|
5843
6007
|
const hash = await this.storeIfPresent(path);
|
|
5844
6008
|
if (hash === null || this.whereFolder() !== "here") return false;
|
|
5845
|
-
this.tracked.set(path, { hash, stat:
|
|
6009
|
+
this.tracked.set(path, { hash, stat: stat3 });
|
|
5846
6010
|
if (known.hash !== hash) await this.record(path, known.hash, hash, at);
|
|
5847
|
-
return known.hash !== hash || known.stat !==
|
|
6011
|
+
return known.hash !== hash || known.stat !== stat3;
|
|
5848
6012
|
}
|
|
5849
6013
|
async record(path, before, after, at) {
|
|
5850
|
-
const endedBeforeThisWrite = this.windows.filter((
|
|
6014
|
+
const endedBeforeThisWrite = this.windows.filter((open2) => !takesWrite(open2, at));
|
|
5851
6015
|
if (endedBeforeThisWrite.length) await this.commitOutside();
|
|
5852
6016
|
for (const window2 of endedBeforeThisWrite) await this.endWindow(window2);
|
|
5853
6017
|
const window = this.windows.at(-1);
|
|
@@ -5880,7 +6044,7 @@ var Engine = class {
|
|
|
5880
6044
|
if (window.changes.size) await this.commitCut(window, [...window.changes.keys()]);
|
|
5881
6045
|
}
|
|
5882
6046
|
pendingElsewhere() {
|
|
5883
|
-
return Boolean(this.outside?.changes.size) || this.windows.some((
|
|
6047
|
+
return Boolean(this.outside?.changes.size) || this.windows.some((open2) => open2.changes.size > 0);
|
|
5884
6048
|
}
|
|
5885
6049
|
/** The held claim `taken` continues: same key, each file from its own last change, no held file changed since. */
|
|
5886
6050
|
heldFor(key, taken) {
|
|
@@ -5894,7 +6058,7 @@ var Engine = class {
|
|
|
5894
6058
|
return group && taken.every((change) => after(change.path, change.before) === change.before) && [...group.changes.values()].every(untouched) ? group : null;
|
|
5895
6059
|
}
|
|
5896
6060
|
logPath(path) {
|
|
5897
|
-
return relative5(this.dir,
|
|
6061
|
+
return relative5(this.dir, resolve8(this.dir, path)).split(sep3).join("/");
|
|
5898
6062
|
}
|
|
5899
6063
|
/** Takes `paths`' uncommitted changes from others, cut at what `who` overwrote. */
|
|
5900
6064
|
async takeClaimed(who, paths, overwrote) {
|
|
@@ -5905,7 +6069,7 @@ var Engine = class {
|
|
|
5905
6069
|
hashOfVersion(version)
|
|
5906
6070
|
])
|
|
5907
6071
|
);
|
|
5908
|
-
const others = this.windows.filter((
|
|
6072
|
+
const others = this.windows.filter((open2) => !sameWho(open2.who, who));
|
|
5909
6073
|
const groups = this.outside ? [this.outside, ...others] : others;
|
|
5910
6074
|
const hits = groups.flatMap(
|
|
5911
6075
|
(group) => [...group.changes.values()].filter((change) => wanted.has(change.path)).map((change) => ({ group, change }))
|
|
@@ -5960,6 +6124,7 @@ var Engine = class {
|
|
|
5960
6124
|
part.changes.set(path, window.changes.get(path));
|
|
5961
6125
|
window.changes.delete(path);
|
|
5962
6126
|
}
|
|
6127
|
+
(window.parts ??= /* @__PURE__ */ new Set()).add(part.id);
|
|
5963
6128
|
window.entry = await this.commit(part);
|
|
5964
6129
|
}
|
|
5965
6130
|
holdClaim(group, key, idleMs = this.options.quietMs ?? 2e3) {
|
|
@@ -6051,7 +6216,7 @@ var Engine = class {
|
|
|
6051
6216
|
}
|
|
6052
6217
|
/** A watcher saw a write: one sweep per burst takes it in (a deleted folder is reported by its name alone). */
|
|
6053
6218
|
noteChange(path) {
|
|
6054
|
-
if (this.notedTimer || !affectsProjectSignature(this.dir,
|
|
6219
|
+
if (this.notedTimer || !affectsProjectSignature(this.dir, resolve8(this.dir, path))) return;
|
|
6055
6220
|
this.notedTimer = setTimeout(() => {
|
|
6056
6221
|
this.notedTimer = null;
|
|
6057
6222
|
this.background(() => this.sweep());
|
|
@@ -6093,7 +6258,7 @@ var Engine = class {
|
|
|
6093
6258
|
if (!this.windows.includes(window)) return window.entry ?? null;
|
|
6094
6259
|
clearTimeout(window.idleTimer);
|
|
6095
6260
|
await this.commitClaim();
|
|
6096
|
-
this.windows = this.windows.filter((
|
|
6261
|
+
this.windows = this.windows.filter((open2) => open2 !== window);
|
|
6097
6262
|
window.entry = await this.commit(window) ?? window.entry ?? null;
|
|
6098
6263
|
return window.entry;
|
|
6099
6264
|
}
|
|
@@ -6109,9 +6274,9 @@ var Engine = class {
|
|
|
6109
6274
|
const parts = path.split("/");
|
|
6110
6275
|
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
6111
6276
|
const at = parts.slice(0, depth).join("/");
|
|
6112
|
-
const
|
|
6113
|
-
if (!
|
|
6114
|
-
if (at === path || !
|
|
6277
|
+
const stat3 = lstatSync4(join19(this.dir, at), { throwIfNoEntry: false });
|
|
6278
|
+
if (!stat3) return void 0;
|
|
6279
|
+
if (at === path || !stat3.isDirectory()) return { at, stat: stat3 };
|
|
6115
6280
|
}
|
|
6116
6281
|
return void 0;
|
|
6117
6282
|
}
|
|
@@ -6121,8 +6286,8 @@ var Engine = class {
|
|
|
6121
6286
|
if (!standing) return void 0;
|
|
6122
6287
|
if (standing.stat.isSymbolicLink()) return standing.at;
|
|
6123
6288
|
if (standing.at === path && !standing.stat.isDirectory()) return void 0;
|
|
6124
|
-
const real =
|
|
6125
|
-
const at = relative5(
|
|
6289
|
+
const real = realpathSync.native(join19(this.dir, standing.at));
|
|
6290
|
+
const at = relative5(realpathSync.native(this.dir), real).split(sep3).join("/");
|
|
6126
6291
|
if (standing.at !== path) return deleted.has(at) ? void 0 : at;
|
|
6127
6292
|
return fileLeftIn(real, at, deleted);
|
|
6128
6293
|
}
|
|
@@ -6139,7 +6304,7 @@ var Engine = class {
|
|
|
6139
6304
|
for (const [path, hash] of changes) await this.writeProjectFile(path, hash);
|
|
6140
6305
|
await this.sweep();
|
|
6141
6306
|
} finally {
|
|
6142
|
-
this.windows = this.windows.filter((
|
|
6307
|
+
this.windows = this.windows.filter((open2) => open2 !== group);
|
|
6143
6308
|
}
|
|
6144
6309
|
return this.commit(group, extra);
|
|
6145
6310
|
}
|
|
@@ -6199,7 +6364,13 @@ var Engine = class {
|
|
|
6199
6364
|
(group) => group?.changes.size && mine(group.who)
|
|
6200
6365
|
);
|
|
6201
6366
|
if (pending) return direction === "back" ? this.pendingEntry(pending) : void 0;
|
|
6202
|
-
|
|
6367
|
+
const everyone = this.options.undoScope === "everyone";
|
|
6368
|
+
const ofOpenTurn = (entry) => this.windows.some((open2) => open2.parts?.has(entry.id));
|
|
6369
|
+
return stepTarget(
|
|
6370
|
+
this.log.entries,
|
|
6371
|
+
direction,
|
|
6372
|
+
(entry) => mine(entry.who) || everyone && !ofOpenTurn(entry)
|
|
6373
|
+
);
|
|
6203
6374
|
}
|
|
6204
6375
|
/** A pending group as the entry it becomes once committed (a window's part gets a fresh id). */
|
|
6205
6376
|
pendingEntry(group) {
|
|
@@ -6270,8 +6441,8 @@ var Engine = class {
|
|
|
6270
6441
|
},
|
|
6271
6442
|
checkout: (entryId, side, emptyDir) => this.queue(async () => {
|
|
6272
6443
|
this.entry(entryId);
|
|
6273
|
-
if (!
|
|
6274
|
-
const dest =
|
|
6444
|
+
if (!existsSync11(this.dir)) throw new Error(`The project folder is gone: ${this.dir}`);
|
|
6445
|
+
const dest = resolve8(emptyDir);
|
|
6275
6446
|
if (isWithin(this.dir, dest))
|
|
6276
6447
|
throw new Error(`Checkout writes outside the project only: ${dest}`);
|
|
6277
6448
|
if ((await readdir2(dest).catch(missingIsEmpty)).length > 0)
|
|
@@ -6316,10 +6487,10 @@ var Engine = class {
|
|
|
6316
6487
|
}
|
|
6317
6488
|
};
|
|
6318
6489
|
function isWithin(dir, path) {
|
|
6319
|
-
let probe =
|
|
6320
|
-
while (!
|
|
6321
|
-
const rel = relative5(
|
|
6322
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
6490
|
+
let probe = resolve8(path);
|
|
6491
|
+
while (!existsSync11(probe) && dirname9(probe) !== probe) probe = dirname9(probe);
|
|
6492
|
+
const rel = relative5(realpathSync.native(dir), realpathSync.native(probe));
|
|
6493
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute2(rel);
|
|
6323
6494
|
}
|
|
6324
6495
|
function missingIsEmpty(error) {
|
|
6325
6496
|
if (error.code === "ENOENT") return [];
|
|
@@ -6332,7 +6503,7 @@ var HistoryClosedError = class extends Error {
|
|
|
6332
6503
|
}
|
|
6333
6504
|
};
|
|
6334
6505
|
async function openProjectHistory(options) {
|
|
6335
|
-
if (!
|
|
6506
|
+
if (!existsSync11(options.projectDir))
|
|
6336
6507
|
throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
|
|
6337
6508
|
const projectId = projectHistoryId(options.projectDir, options.historyRoot);
|
|
6338
6509
|
const release = await takeHistoryOwnership(
|
|
@@ -6499,6 +6670,7 @@ function createStudioApi(adapter) {
|
|
|
6499
6670
|
registerLintRoutes(api, adapter);
|
|
6500
6671
|
registerRenderRoutes(api, adapter);
|
|
6501
6672
|
registerThumbnailRoutes(api, adapter);
|
|
6673
|
+
registerImageThumbnailRoutes(api, adapter);
|
|
6502
6674
|
registerSelectionRoutes(api, adapter);
|
|
6503
6675
|
registerMediaRoutes(api, adapter);
|
|
6504
6676
|
registerWaveformRoutes(api, adapter);
|
|
@@ -6510,14 +6682,14 @@ function createStudioApi(adapter) {
|
|
|
6510
6682
|
}
|
|
6511
6683
|
|
|
6512
6684
|
// src/history/historyCache.ts
|
|
6513
|
-
function historyCache(
|
|
6685
|
+
function historyCache(open2) {
|
|
6514
6686
|
const histories = /* @__PURE__ */ new Map();
|
|
6515
6687
|
const get = async (projectDir) => {
|
|
6516
6688
|
let cached = histories.get(projectDir);
|
|
6517
|
-
if (!cached) histories.set(projectDir, cached =
|
|
6689
|
+
if (!cached) histories.set(projectDir, cached = open2(projectDir));
|
|
6518
6690
|
const opened = await cached;
|
|
6519
6691
|
if (!opened?.replacedAtPath()) return opened;
|
|
6520
|
-
const reopen = () =>
|
|
6692
|
+
const reopen = () => open2(projectDir);
|
|
6521
6693
|
if (histories.get(projectDir) === cached)
|
|
6522
6694
|
histories.set(projectDir, opened.close().then(reopen, reopen));
|
|
6523
6695
|
return get(projectDir);
|
|
@@ -6534,6 +6706,7 @@ function historyCache(open) {
|
|
|
6534
6706
|
}
|
|
6535
6707
|
|
|
6536
6708
|
// src/helpers/duplicateElement.ts
|
|
6709
|
+
import { sameInstant } from "@hyperframes/core/clip-facts";
|
|
6537
6710
|
import { ensureHfIds as ensureHfIds4 } from "@hyperframes/parsers/hf-ids";
|
|
6538
6711
|
function duplicateElementInHtml(source, target, newId, at) {
|
|
6539
6712
|
const { document, wrappedFragment } = parseSourceDocument(source);
|
|
@@ -6568,9 +6741,10 @@ function rippleElements(document, element, at, duration, track) {
|
|
|
6568
6741
|
continue;
|
|
6569
6742
|
}
|
|
6570
6743
|
const start = numericAttribute(candidate, "data-start");
|
|
6571
|
-
if (start
|
|
6572
|
-
|
|
6573
|
-
|
|
6744
|
+
if (start === null) continue;
|
|
6745
|
+
const metInsertionPoint = sameInstant(start, at);
|
|
6746
|
+
if (start < at && !metInsertionPoint) continue;
|
|
6747
|
+
candidate.setAttribute("data-start", String((metInsertionPoint ? at : start) + duration));
|
|
6574
6748
|
}
|
|
6575
6749
|
}
|
|
6576
6750
|
function stripStableIds(element) {
|
|
@@ -6585,11 +6759,11 @@ function numericAttribute(element, name) {
|
|
|
6585
6759
|
}
|
|
6586
6760
|
|
|
6587
6761
|
// src/helpers/previewDocumentStore.ts
|
|
6588
|
-
import { createHash as
|
|
6589
|
-
import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as
|
|
6762
|
+
import { createHash as createHash8 } from "crypto";
|
|
6763
|
+
import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync7, rmSync as rmSync7 } from "fs";
|
|
6590
6764
|
import { join as join20 } from "path";
|
|
6591
6765
|
function createPreviewDocumentStore(dir, salt) {
|
|
6592
|
-
const fileFor = (key) => join20(dir, `${
|
|
6766
|
+
const fileFor = (key) => join20(dir, `${createHash8("sha256").update(`${salt}
|
|
6593
6767
|
${key}`).digest("hex").slice(0, 32)}.html`);
|
|
6594
6768
|
return {
|
|
6595
6769
|
read(key) {
|
|
@@ -6604,7 +6778,7 @@ ${key}`).digest("hex").slice(0, 32)}.html`);
|
|
|
6604
6778
|
mkdirSync12(dir, { recursive: true });
|
|
6605
6779
|
const file = fileFor(key);
|
|
6606
6780
|
replaceFileAtomically(file, html, 420);
|
|
6607
|
-
for (const name of
|
|
6781
|
+
for (const name of readdirSync7(dir)) {
|
|
6608
6782
|
if (join20(dir, name) !== file) rmSync7(join20(dir, name), { force: true });
|
|
6609
6783
|
}
|
|
6610
6784
|
} catch {
|
|
@@ -6686,12 +6860,14 @@ export {
|
|
|
6686
6860
|
START as HISTORY_START,
|
|
6687
6861
|
HistoryBusyError,
|
|
6688
6862
|
HistoryClosedError,
|
|
6863
|
+
HistoryIdError,
|
|
6689
6864
|
MAX_WINDOW_IDLE_MS,
|
|
6690
6865
|
MIME_TYPES,
|
|
6691
6866
|
PREVIEW_BUNDLE_OPTIONS,
|
|
6692
6867
|
STUDIO_MANUAL_EDITS_PATH,
|
|
6693
6868
|
STUDIO_MOTION_PATH,
|
|
6694
6869
|
UNDO_MODES,
|
|
6870
|
+
affectsPreview,
|
|
6695
6871
|
affectsProjectSignature,
|
|
6696
6872
|
applyFileMutations,
|
|
6697
6873
|
buildSubCompositionHtml,
|