@hyperframes/studio-server 0.8.74 → 0.8.76
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-PGL4S2ZU.js → chunk-5PGWTBCL.js} +1 -1
- package/dist/chunk-5PGWTBCL.js.map +1 -0
- package/dist/{chunk-KFPWE4Q7.js → chunk-QYDRVMJ6.js} +3 -2
- package/dist/chunk-QYDRVMJ6.js.map +1 -0
- package/dist/helpers/mediaProxyPreview.d.ts +2 -1
- package/dist/helpers/mediaProxyPreview.js +1 -1
- package/dist/helpers/sourceMutation.d.ts +1 -1
- package/dist/helpers/sourceMutation.js +1 -1
- package/dist/index.d.ts +18 -4
- package/dist/index.js +626 -330
- package/dist/index.js.map +1 -1
- package/dist/{mediaProxyPreview-BXtfHq7G.d.ts → mediaProxyPreview-DZD3iu_2.d.ts} +37 -11
- package/package.json +3 -3
- package/dist/chunk-KFPWE4Q7.js.map +0 -1
- package/dist/chunk-PGL4S2ZU.js.map +0 -1
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-QYDRVMJ6.js";
|
|
17
17
|
import {
|
|
18
18
|
getElementScreenshotClip
|
|
19
19
|
} from "./chunk-W2SBTCO2.js";
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
isAutoProxyEnabled,
|
|
23
23
|
proxyEtagSalt,
|
|
24
24
|
resolvePreviewMediaCodecProbeCache
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-5PGWTBCL.js";
|
|
26
26
|
import {
|
|
27
27
|
ProxyCapacityError,
|
|
28
28
|
ProxyTranscodeError,
|
|
@@ -52,7 +52,7 @@ import { Hono } from "hono";
|
|
|
52
52
|
|
|
53
53
|
// src/routes/projects.ts
|
|
54
54
|
import { readFile } from "fs/promises";
|
|
55
|
-
import { join as
|
|
55
|
+
import { join as join3 } from "path";
|
|
56
56
|
|
|
57
57
|
// src/helpers/safePath.ts
|
|
58
58
|
import { join } from "path";
|
|
@@ -86,9 +86,113 @@ function walkDir(dir, prefix = "") {
|
|
|
86
86
|
return files;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// src/helpers/
|
|
89
|
+
// src/helpers/hfIdPersist.ts
|
|
90
|
+
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
90
91
|
import { createHash } from "crypto";
|
|
91
|
-
import {
|
|
92
|
+
import {
|
|
93
|
+
closeSync,
|
|
94
|
+
constants,
|
|
95
|
+
fstatSync,
|
|
96
|
+
mkdirSync,
|
|
97
|
+
openSync,
|
|
98
|
+
readFileSync,
|
|
99
|
+
writeFileSync
|
|
100
|
+
} from "fs";
|
|
101
|
+
import { dirname, join as join2 } from "path";
|
|
102
|
+
|
|
103
|
+
// src/helpers/atomicFile.ts
|
|
104
|
+
import * as fs from "fs";
|
|
105
|
+
import { randomUUID } from "crypto";
|
|
106
|
+
function replaceFileAtomically(filePath, content, mode, operations = fs) {
|
|
107
|
+
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
108
|
+
try {
|
|
109
|
+
operations.writeFileSync(tempPath, content, { encoding: "utf-8", mode });
|
|
110
|
+
operations.chmodSync(tempPath, mode);
|
|
111
|
+
operations.renameSync(tempPath, filePath);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
try {
|
|
114
|
+
operations.unlinkSync(tempPath);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/helpers/hfIdPersist.ts
|
|
122
|
+
var isCompositionSource = (html) => /data-composition-id\s*=/.test(html);
|
|
123
|
+
var STAMP_RECORD = join2(".hyperframes", "hf-ids-stamped.json");
|
|
124
|
+
var contentHash = (text2) => createHash("sha1").update(text2).digest("base64url");
|
|
125
|
+
function stampProjectHfIds(projectDir) {
|
|
126
|
+
const recordPath = join2(projectDir, STAMP_RECORD);
|
|
127
|
+
let last = {};
|
|
128
|
+
try {
|
|
129
|
+
last = JSON.parse(readFileSync(recordPath, "utf-8"));
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
const next = {};
|
|
133
|
+
for (const file of walkDir(projectDir)) {
|
|
134
|
+
if (!file.endsWith(".html") || isInHiddenOrVendorDir(file)) continue;
|
|
135
|
+
const absPath = join2(projectDir, file);
|
|
136
|
+
let html;
|
|
137
|
+
try {
|
|
138
|
+
html = readFileSync(absPath, "utf-8");
|
|
139
|
+
} catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
let hash = contentHash(html);
|
|
143
|
+
if (last[file] !== hash && isCompositionSource(html)) {
|
|
144
|
+
const stamped = stampFileHfIds(absPath);
|
|
145
|
+
if (stamped === null) continue;
|
|
146
|
+
hash = contentHash(stamped);
|
|
147
|
+
}
|
|
148
|
+
next[file] = hash;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
mkdirSync(dirname(recordPath), { recursive: true });
|
|
152
|
+
writeFileSync(recordPath, JSON.stringify(next));
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function openNoFollow(filePath, flags) {
|
|
157
|
+
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
158
|
+
try {
|
|
159
|
+
return openSync(filePath, flags | noFollow);
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function stampFileHfIds(filePath) {
|
|
165
|
+
let fd = openNoFollow(filePath, constants.O_RDWR);
|
|
166
|
+
let writable = true;
|
|
167
|
+
if (fd === null) {
|
|
168
|
+
fd = openNoFollow(filePath, constants.O_RDONLY);
|
|
169
|
+
writable = false;
|
|
170
|
+
}
|
|
171
|
+
if (fd === null) return null;
|
|
172
|
+
try {
|
|
173
|
+
if (!fstatSync(fd).isFile()) return null;
|
|
174
|
+
const html = readFileSync(fd, "utf-8");
|
|
175
|
+
const normalized = ensureHfIds(html);
|
|
176
|
+
const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
|
|
177
|
+
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
178
|
+
if (writable && idsAfter > idsBefore) {
|
|
179
|
+
const mode = fstatSync(fd).mode;
|
|
180
|
+
closeSync(fd);
|
|
181
|
+
fd = null;
|
|
182
|
+
replaceFileAtomically(filePath, normalized, mode);
|
|
183
|
+
}
|
|
184
|
+
return normalized;
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.warn("[hyperframes] stampFileHfIds: failed to stamp ids:", err);
|
|
187
|
+
return null;
|
|
188
|
+
} finally {
|
|
189
|
+
if (fd !== null) closeSync(fd);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/helpers/projectSignature.ts
|
|
194
|
+
import { createHash as createHash2 } from "crypto";
|
|
195
|
+
import { lstatSync, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
92
196
|
import { extname, isAbsolute, relative, resolve, sep } from "path";
|
|
93
197
|
var SIGNATURE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
94
198
|
".cjs",
|
|
@@ -194,7 +298,7 @@ function collectProjectSignatureManifestFiles(projectDir, files) {
|
|
|
194
298
|
}
|
|
195
299
|
}
|
|
196
300
|
function createProjectFingerprint(projectDir, files) {
|
|
197
|
-
const hash =
|
|
301
|
+
const hash = createHash2("sha256");
|
|
198
302
|
for (const entry of files) {
|
|
199
303
|
hash.update(relative(projectDir, entry.file));
|
|
200
304
|
hash.update("\0");
|
|
@@ -240,10 +344,10 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
|
|
|
240
344
|
);
|
|
241
345
|
files.sort((a, b) => a.file.localeCompare(b.file));
|
|
242
346
|
const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
|
|
243
|
-
const cacheKey = excluding.size ? `${normalizedProjectDir}\0${
|
|
347
|
+
const cacheKey = excluding.size ? `${normalizedProjectDir}\0${createHash2("sha256").update([...excluding].sort().join("\0")).digest("hex")}` : normalizedProjectDir;
|
|
244
348
|
const cached = projectSignatureCache.get(cacheKey);
|
|
245
349
|
if (cached?.fingerprint === fingerprint) return cached.signature;
|
|
246
|
-
const hash =
|
|
350
|
+
const hash = createHash2("sha256");
|
|
247
351
|
for (const entry of files) {
|
|
248
352
|
const relativePath = relative(normalizedProjectDir, entry.file);
|
|
249
353
|
hash.update(relativePath);
|
|
@@ -252,7 +356,7 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
|
|
|
252
356
|
hash.update("\0");
|
|
253
357
|
if (entry.textContentEligible) {
|
|
254
358
|
try {
|
|
255
|
-
hash.update(
|
|
359
|
+
hash.update(readFileSync2(entry.file));
|
|
256
360
|
} catch {
|
|
257
361
|
hash.update(String(entry.mtimeMs));
|
|
258
362
|
}
|
|
@@ -267,14 +371,13 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
|
|
|
267
371
|
}
|
|
268
372
|
|
|
269
373
|
// src/routes/projects.ts
|
|
270
|
-
var COMPOSITION_ID_RE = /data-composition-id\s*=/;
|
|
271
374
|
async function filterCompositionFiles(projectDir, files) {
|
|
272
375
|
const htmlFiles = files.filter((f) => f.endsWith(".html") && !isInHiddenOrVendorDir(f));
|
|
273
376
|
const checks = await Promise.all(
|
|
274
377
|
htmlFiles.map(async (f) => {
|
|
275
378
|
try {
|
|
276
|
-
const content = await readFile(
|
|
277
|
-
return
|
|
379
|
+
const content = await readFile(join3(projectDir, f), "utf-8");
|
|
380
|
+
return isCompositionSource(content);
|
|
278
381
|
} catch {
|
|
279
382
|
return false;
|
|
280
383
|
}
|
|
@@ -313,23 +416,23 @@ function registerProjectRoutes(api, adapter) {
|
|
|
313
416
|
// src/routes/files.ts
|
|
314
417
|
import { bodyLimit } from "hono/body-limit";
|
|
315
418
|
import {
|
|
316
|
-
closeSync,
|
|
419
|
+
closeSync as closeSync2,
|
|
317
420
|
existsSync as existsSync3,
|
|
318
421
|
lstatSync as lstatSync3,
|
|
319
|
-
openSync,
|
|
320
|
-
readFileSync as
|
|
422
|
+
openSync as openSync2,
|
|
423
|
+
readFileSync as readFileSync6,
|
|
321
424
|
readlinkSync,
|
|
322
|
-
writeFileSync as
|
|
425
|
+
writeFileSync as writeFileSync5,
|
|
323
426
|
writeSync,
|
|
324
|
-
mkdirSync as
|
|
427
|
+
mkdirSync as mkdirSync4,
|
|
325
428
|
unlinkSync as unlinkSync2,
|
|
326
429
|
rmSync as rmSync3,
|
|
327
430
|
statSync as statSync3,
|
|
328
|
-
fstatSync,
|
|
431
|
+
fstatSync as fstatSync2,
|
|
329
432
|
renameSync as renameSync2,
|
|
330
433
|
readdirSync as readdirSync4
|
|
331
434
|
} from "fs";
|
|
332
|
-
import { resolve as resolve3, dirname as
|
|
435
|
+
import { resolve as resolve3, dirname as dirname4, join as join7 } from "path";
|
|
333
436
|
|
|
334
437
|
// src/helpers/mime.ts
|
|
335
438
|
var MIME_TYPES = {
|
|
@@ -377,37 +480,19 @@ function isAudioFile(name) {
|
|
|
377
480
|
return (getMimeType(name) ?? "").startsWith("audio/");
|
|
378
481
|
}
|
|
379
482
|
|
|
380
|
-
// src/helpers/atomicFile.ts
|
|
381
|
-
import * as fs from "fs";
|
|
382
|
-
import { randomUUID } from "crypto";
|
|
383
|
-
function replaceFileAtomically(filePath, content, mode, operations = fs) {
|
|
384
|
-
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
385
|
-
try {
|
|
386
|
-
operations.writeFileSync(tempPath, content, { encoding: "utf-8", mode });
|
|
387
|
-
operations.chmodSync(tempPath, mode);
|
|
388
|
-
operations.renameSync(tempPath, filePath);
|
|
389
|
-
} catch (error) {
|
|
390
|
-
try {
|
|
391
|
-
operations.unlinkSync(tempPath);
|
|
392
|
-
} catch {
|
|
393
|
-
}
|
|
394
|
-
throw error;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
483
|
// src/helpers/waveform.ts
|
|
399
484
|
import { spawn } from "child_process";
|
|
400
485
|
import {
|
|
401
486
|
existsSync,
|
|
402
|
-
writeFileSync,
|
|
403
|
-
mkdirSync,
|
|
487
|
+
writeFileSync as writeFileSync2,
|
|
488
|
+
mkdirSync as mkdirSync2,
|
|
404
489
|
lstatSync as lstatSync2,
|
|
405
490
|
mkdtempSync,
|
|
406
491
|
renameSync,
|
|
407
492
|
rmSync,
|
|
408
493
|
statSync
|
|
409
494
|
} from "fs";
|
|
410
|
-
import { dirname, join as
|
|
495
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
411
496
|
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
412
497
|
var SAMPLE_RATE = 4e3;
|
|
413
498
|
var PEAK_COUNT = 4e3;
|
|
@@ -474,11 +559,11 @@ function decodeAudioPeaks(audioPath) {
|
|
|
474
559
|
});
|
|
475
560
|
}
|
|
476
561
|
async function generateWaveformCache(projectDir, assetPath) {
|
|
477
|
-
const audioPath =
|
|
562
|
+
const audioPath = join4(projectDir, assetPath);
|
|
478
563
|
if (!existsSync(audioPath)) return;
|
|
479
564
|
const stats = statSync(audioPath);
|
|
480
|
-
const cacheDir =
|
|
481
|
-
const cachePath =
|
|
565
|
+
const cacheDir = join4(projectDir, ".waveform-cache");
|
|
566
|
+
const cachePath = join4(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
482
567
|
if (isWaveformCacheDirectory(cacheDir) && existsSync(cachePath)) return;
|
|
483
568
|
const peaks = await decodeAudioPeaks(audioPath);
|
|
484
569
|
writeWaveformCache(cachePath, peaks);
|
|
@@ -487,19 +572,19 @@ function isWaveformCacheDirectory(cacheDir) {
|
|
|
487
572
|
return lstatSync2(cacheDir, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
488
573
|
}
|
|
489
574
|
function writeWaveformCache(cachePath, peaks) {
|
|
490
|
-
const cacheDir =
|
|
575
|
+
const cacheDir = dirname2(cachePath);
|
|
491
576
|
try {
|
|
492
|
-
|
|
577
|
+
mkdirSync2(cacheDir, { mode: 448 });
|
|
493
578
|
} catch (error) {
|
|
494
579
|
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
495
580
|
}
|
|
496
581
|
if (!isWaveformCacheDirectory(cacheDir)) {
|
|
497
582
|
throw new Error("Waveform cache must be a directory, not a symlink");
|
|
498
583
|
}
|
|
499
|
-
const stagingDir = mkdtempSync(
|
|
584
|
+
const stagingDir = mkdtempSync(join4(cacheDir, ".waveform-"));
|
|
500
585
|
try {
|
|
501
|
-
const stagingPath =
|
|
502
|
-
|
|
586
|
+
const stagingPath = join4(stagingDir, "peaks.json");
|
|
587
|
+
writeFileSync2(stagingPath, JSON.stringify(peaks), { flag: "wx" });
|
|
503
588
|
renameSync(stagingPath, cachePath);
|
|
504
589
|
} finally {
|
|
505
590
|
try {
|
|
@@ -511,9 +596,9 @@ function writeWaveformCache(cachePath, peaks) {
|
|
|
511
596
|
|
|
512
597
|
// src/helpers/mediaValidation.ts
|
|
513
598
|
import { spawnSync } from "child_process";
|
|
514
|
-
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as
|
|
599
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
515
600
|
import { tmpdir } from "os";
|
|
516
|
-
import { basename, join as
|
|
601
|
+
import { basename, join as join5 } from "path";
|
|
517
602
|
var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
|
|
518
603
|
var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
|
|
519
604
|
function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
@@ -550,10 +635,10 @@ function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
|
550
635
|
}
|
|
551
636
|
}
|
|
552
637
|
function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
|
|
553
|
-
const tempDir = mkdtempSync2(
|
|
554
|
-
const tempPath =
|
|
638
|
+
const tempDir = mkdtempSync2(join5(tmpdir(), "hyperframes-upload-"));
|
|
639
|
+
const tempPath = join5(tempDir, basename(fileName));
|
|
555
640
|
try {
|
|
556
|
-
|
|
641
|
+
writeFileSync3(tempPath, buffer);
|
|
557
642
|
return validateUploadedMedia(tempPath, runner);
|
|
558
643
|
} finally {
|
|
559
644
|
rmSync2(tempDir, { recursive: true, force: true });
|
|
@@ -561,9 +646,9 @@ function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
|
|
|
561
646
|
}
|
|
562
647
|
|
|
563
648
|
// src/helpers/backupJournal.ts
|
|
564
|
-
import { mkdirSync as
|
|
649
|
+
import { mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
565
650
|
import { Buffer as Buffer2 } from "buffer";
|
|
566
|
-
import { join as
|
|
651
|
+
import { join as join6, relative as relative2 } from "path";
|
|
567
652
|
var DEFAULT_KEEP_PER_FILE = 10;
|
|
568
653
|
function backupKeyForPath(path) {
|
|
569
654
|
return Buffer2.from(path, "utf-8").toString("base64url");
|
|
@@ -580,13 +665,13 @@ function backupPathForResponse(projectDir, backupPath) {
|
|
|
580
665
|
function snapshotBeforeWrite(projectDir, absPath, options = {}) {
|
|
581
666
|
if (!isSafePath(projectDir, absPath)) return { backupPath: null };
|
|
582
667
|
try {
|
|
583
|
-
const content =
|
|
668
|
+
const content = readFileSync3(absPath);
|
|
584
669
|
const relativePath = relative2(projectDir, absPath);
|
|
585
|
-
const backupDir =
|
|
586
|
-
|
|
670
|
+
const backupDir = join6(projectDir, ".hyperframes", "backup");
|
|
671
|
+
mkdirSync3(backupDir, { recursive: true });
|
|
587
672
|
const backupKey = backupKeyForPath(relativePath);
|
|
588
673
|
const backupPath = nextBackupPath(backupDir, backupKey);
|
|
589
|
-
|
|
674
|
+
writeFileSync4(backupPath, content);
|
|
590
675
|
pruneBackups(backupDir, backupKey, options.keepPerFile ?? DEFAULT_KEEP_PER_FILE);
|
|
591
676
|
return { backupPath };
|
|
592
677
|
} catch (error) {
|
|
@@ -598,18 +683,18 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
|
|
|
598
683
|
}
|
|
599
684
|
function nextBackupPath(backupDir, backupKey) {
|
|
600
685
|
const base = `${timestampPrefix()}-${backupKey}`;
|
|
601
|
-
let candidate =
|
|
686
|
+
let candidate = join6(backupDir, base);
|
|
602
687
|
let counter = 2;
|
|
603
688
|
while (true) {
|
|
604
689
|
try {
|
|
605
|
-
|
|
690
|
+
readFileSync3(candidate);
|
|
606
691
|
} catch (error) {
|
|
607
692
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
608
693
|
return candidate;
|
|
609
694
|
}
|
|
610
695
|
throw error;
|
|
611
696
|
}
|
|
612
|
-
candidate =
|
|
697
|
+
candidate = join6(backupDir, `${base}-${counter}`);
|
|
613
698
|
counter += 1;
|
|
614
699
|
}
|
|
615
700
|
}
|
|
@@ -617,7 +702,7 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
|
|
|
617
702
|
const keep = Math.max(1, Math.floor(keepPerFile));
|
|
618
703
|
const suffix = `-${backupKey}`;
|
|
619
704
|
const numberedSuffix = new RegExp(`-${backupKey}-\\d+$`);
|
|
620
|
-
const matches = readdirSync3(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) =>
|
|
705
|
+
const matches = readdirSync3(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join6(backupDir, name)).sort((a, b) => {
|
|
621
706
|
return b.localeCompare(a);
|
|
622
707
|
});
|
|
623
708
|
for (const file of matches.slice(keep)) {
|
|
@@ -629,11 +714,18 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
|
|
|
629
714
|
}
|
|
630
715
|
|
|
631
716
|
// src/helpers/fileVersion.ts
|
|
632
|
-
import { createHash as
|
|
717
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
633
718
|
var RECEIPT_TTL_MS = 1e4;
|
|
634
719
|
var receipts = /* @__PURE__ */ new Map();
|
|
635
720
|
function fileContentVersion(content) {
|
|
636
|
-
return
|
|
721
|
+
return hashVersion(createHash3("sha256").update(content).digest("hex"));
|
|
722
|
+
}
|
|
723
|
+
var DELETED_VERSION = '"deleted"';
|
|
724
|
+
function hashVersion(hex) {
|
|
725
|
+
return `"sha256:${hex}"`;
|
|
726
|
+
}
|
|
727
|
+
function hashOfVersion(version) {
|
|
728
|
+
return /^"sha256:([0-9a-f]{64})"$/.exec(version)?.[1];
|
|
637
729
|
}
|
|
638
730
|
function settledFileTag(stat2, now = Date.now()) {
|
|
639
731
|
if (now - stat2.ctimeMs < 3e3) return null;
|
|
@@ -676,11 +768,11 @@ function identifyFileWrite(absPath, expectedVersion) {
|
|
|
676
768
|
var consumeFileWriteReceipt = identifyFileWrite;
|
|
677
769
|
|
|
678
770
|
// src/helpers/applyFileMutations.ts
|
|
679
|
-
import { readFileSync as
|
|
771
|
+
import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
680
772
|
function applyFileMutations(projectDir, mutations, requestToken, writeFile = (path, content) => replaceFileAtomically(path, content, statSync2(path).mode)) {
|
|
681
773
|
const prepared = mutations.map((mutation) => ({
|
|
682
774
|
...mutation,
|
|
683
|
-
before: mutation.before ??
|
|
775
|
+
before: mutation.before ?? readFileSync4(mutation.absPath, "utf-8")
|
|
684
776
|
}));
|
|
685
777
|
const results = [];
|
|
686
778
|
const attempted = [];
|
|
@@ -709,7 +801,7 @@ function applyFileMutations(projectDir, mutations, requestToken, writeFile = (pa
|
|
|
709
801
|
}
|
|
710
802
|
}
|
|
711
803
|
function applyOneMutation(projectDir, mutation, requestToken, writeFile, attempted) {
|
|
712
|
-
const current =
|
|
804
|
+
const current = readFileSync4(mutation.absPath, "utf-8");
|
|
713
805
|
assertExpectedVersion(mutation.expectedVersion, current);
|
|
714
806
|
if (mutation.after === mutation.before) {
|
|
715
807
|
return {
|
|
@@ -781,11 +873,12 @@ import {
|
|
|
781
873
|
syncPositionHoldsBeforeKeyframes
|
|
782
874
|
} from "@hyperframes/parsers/gsap-writer-acorn";
|
|
783
875
|
import { parseHTML as parseHTML2 } from "linkedom";
|
|
876
|
+
import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
|
|
784
877
|
|
|
785
878
|
// src/helpers/compositionInsertion.ts
|
|
786
|
-
import { existsSync as existsSync2, readFileSync as
|
|
879
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5, realpathSync } from "fs";
|
|
787
880
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
788
|
-
import { dirname as
|
|
881
|
+
import { dirname as dirname3, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
|
|
789
882
|
import { parseHTML } from "linkedom";
|
|
790
883
|
var CompositionInsertionError = class extends Error {
|
|
791
884
|
constructor(message, status) {
|
|
@@ -840,7 +933,7 @@ function canonicalDependency(projectDir, ownerAbs, sourcePath) {
|
|
|
840
933
|
validateSourcePath(sourcePath);
|
|
841
934
|
return canonicalProjectPath(
|
|
842
935
|
projectDir,
|
|
843
|
-
resolveWithinProject(projectDir, relative3(projectDir, resolve2(
|
|
936
|
+
resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname3(ownerAbs), sourcePath)))
|
|
844
937
|
);
|
|
845
938
|
}
|
|
846
939
|
function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
|
|
@@ -855,7 +948,7 @@ function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
|
|
|
855
948
|
}
|
|
856
949
|
if (visited.has(file)) return;
|
|
857
950
|
visiting.add(file);
|
|
858
|
-
const source =
|
|
951
|
+
const source = readFileSync5(file, "utf-8");
|
|
859
952
|
const { document } = compositionRoot(source);
|
|
860
953
|
for (const host of descendants(document, "[data-composition-src]")) {
|
|
861
954
|
const dependency = host.getAttribute("data-composition-src");
|
|
@@ -913,13 +1006,13 @@ function uniqueHostId(root, base) {
|
|
|
913
1006
|
return `${base}_${suffix}`;
|
|
914
1007
|
}
|
|
915
1008
|
function relativeSourcePath(targetAbs, sourceAbs) {
|
|
916
|
-
return relative3(
|
|
1009
|
+
return relative3(dirname3(targetAbs), sourceAbs).split(sep2).join("/");
|
|
917
1010
|
}
|
|
918
1011
|
function insertCompositionIntoSource(input) {
|
|
919
1012
|
const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
|
|
920
1013
|
const sourceAbs = canonicalProjectFile(input.projectDir, input.sourcePath);
|
|
921
1014
|
validateDependencyGraph(input.projectDir, targetAbs, sourceAbs);
|
|
922
|
-
const source =
|
|
1015
|
+
const source = readFileSync5(sourceAbs, "utf-8");
|
|
923
1016
|
const sourceComposition = compositionRoot(source).root;
|
|
924
1017
|
const duration = positiveAttribute(
|
|
925
1018
|
sourceComposition,
|
|
@@ -1044,7 +1137,7 @@ async function loadGsapParser() {
|
|
|
1044
1137
|
return import("@hyperframes/parsers/gsap-parser-recast");
|
|
1045
1138
|
}
|
|
1046
1139
|
function isDanglingSymlinkInProject(projectDir, lexicalPath) {
|
|
1047
|
-
if (!isSafePath(projectDir,
|
|
1140
|
+
if (!isSafePath(projectDir, dirname4(lexicalPath))) return false;
|
|
1048
1141
|
let stats;
|
|
1049
1142
|
try {
|
|
1050
1143
|
stats = lstatSync3(lexicalPath);
|
|
@@ -1052,7 +1145,7 @@ function isDanglingSymlinkInProject(projectDir, lexicalPath) {
|
|
|
1052
1145
|
return false;
|
|
1053
1146
|
}
|
|
1054
1147
|
if (!stats.isSymbolicLink()) return false;
|
|
1055
|
-
const target = resolve3(
|
|
1148
|
+
const target = resolve3(dirname4(lexicalPath), readlinkSync(lexicalPath));
|
|
1056
1149
|
if (!isSafePath(projectDir, target)) return false;
|
|
1057
1150
|
try {
|
|
1058
1151
|
statSync3(lexicalPath);
|
|
@@ -1151,7 +1244,7 @@ function commitElementPatchBatches(projectDir, batches, writeFile = (path, conte
|
|
|
1151
1244
|
resolvedPaths.add(absPath);
|
|
1152
1245
|
let before;
|
|
1153
1246
|
try {
|
|
1154
|
-
before =
|
|
1247
|
+
before = readFileSync6(absPath, "utf-8");
|
|
1155
1248
|
} catch {
|
|
1156
1249
|
return { error: "not-found", sourceFile: batch.sourceFile };
|
|
1157
1250
|
}
|
|
@@ -1202,7 +1295,7 @@ function commitElementPatchBatchesWithReceipts(c, projectDir, batches) {
|
|
|
1202
1295
|
return commitElementPatchBatches(
|
|
1203
1296
|
projectDir,
|
|
1204
1297
|
batches,
|
|
1205
|
-
|
|
1298
|
+
writeFileSync5,
|
|
1206
1299
|
c.req.header("X-Hyperframes-Write-Token")
|
|
1207
1300
|
);
|
|
1208
1301
|
}
|
|
@@ -1260,8 +1353,8 @@ async function parseMutationBody(c) {
|
|
|
1260
1353
|
return { target: body.target, body };
|
|
1261
1354
|
}
|
|
1262
1355
|
function ensureDir(filePath) {
|
|
1263
|
-
const dir =
|
|
1264
|
-
if (!existsSync3(dir))
|
|
1356
|
+
const dir = dirname4(filePath);
|
|
1357
|
+
if (!existsSync3(dir)) mkdirSync4(dir, { recursive: true });
|
|
1265
1358
|
}
|
|
1266
1359
|
function generateCopyPath(projectDir, originalPath) {
|
|
1267
1360
|
const ext = originalPath.includes(".") ? "." + originalPath.split(".").pop() : "";
|
|
@@ -1279,7 +1372,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1279
1372
|
function walkFiles(dir, filter) {
|
|
1280
1373
|
const results = [];
|
|
1281
1374
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
1282
|
-
const full =
|
|
1375
|
+
const full = join7(dir, entry.name);
|
|
1283
1376
|
if (entry.isDirectory()) {
|
|
1284
1377
|
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders" || entry.name === ".transcode-cache")
|
|
1285
1378
|
continue;
|
|
@@ -1298,7 +1391,7 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
1298
1391
|
let updatedCount = 0;
|
|
1299
1392
|
for (const file of textFiles) {
|
|
1300
1393
|
if (!isSafePath(projectDir, file)) continue;
|
|
1301
|
-
const content =
|
|
1394
|
+
const content = readFileSync6(file, "utf-8");
|
|
1302
1395
|
if (!content.includes(oldPath)) continue;
|
|
1303
1396
|
const updated = content.split(oldPath).join(newPath);
|
|
1304
1397
|
if (updated !== content) {
|
|
@@ -1309,7 +1402,7 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
1309
1402
|
return updatedCount;
|
|
1310
1403
|
}
|
|
1311
1404
|
function extractGsapScriptBlock(html) {
|
|
1312
|
-
const { document } = parseHTML2(html);
|
|
1405
|
+
const { document } = parseHTML2(ensureHfIds2(html));
|
|
1313
1406
|
const scripts = [
|
|
1314
1407
|
...document.querySelectorAll("script:not([src])"),
|
|
1315
1408
|
...Array.from(document.querySelectorAll("template")).flatMap(
|
|
@@ -1572,7 +1665,7 @@ function containsRawGsapExpression(value) {
|
|
|
1572
1665
|
return Object.values(value).some(containsRawGsapExpression);
|
|
1573
1666
|
}
|
|
1574
1667
|
async function prepareGsapMutationScript(c, res, firstMutation) {
|
|
1575
|
-
const beforeHtml =
|
|
1668
|
+
const beforeHtml = readFileSync6(res.absPath, "utf-8");
|
|
1576
1669
|
let html = beforeHtml;
|
|
1577
1670
|
let block = extractGsapScriptBlock(html);
|
|
1578
1671
|
if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
|
|
@@ -1639,7 +1732,7 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1639
1732
|
const changed = block.scriptText !== initialScript;
|
|
1640
1733
|
const newHtml = changed ? block.replaceScript(block.scriptText) : html;
|
|
1641
1734
|
let backupPath = null;
|
|
1642
|
-
if (
|
|
1735
|
+
if (readFileSync6(res.absPath, "utf-8") !== beforeHtml) {
|
|
1643
1736
|
return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
|
|
1644
1737
|
}
|
|
1645
1738
|
if (changed) {
|
|
@@ -2420,11 +2513,11 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2420
2513
|
let written = false;
|
|
2421
2514
|
while (n < MAX_COPY_INDEX && isSafePath(projectDir, finalPath)) {
|
|
2422
2515
|
try {
|
|
2423
|
-
const fd =
|
|
2516
|
+
const fd = openSync2(finalPath, "wx");
|
|
2424
2517
|
try {
|
|
2425
|
-
|
|
2518
|
+
writeFileSync5(fd, buffer);
|
|
2426
2519
|
} finally {
|
|
2427
|
-
|
|
2520
|
+
closeSync2(fd);
|
|
2428
2521
|
}
|
|
2429
2522
|
written = true;
|
|
2430
2523
|
break;
|
|
@@ -2441,7 +2534,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2441
2534
|
if (n >= MAX_COPY_INDEX) skipped.push(name);
|
|
2442
2535
|
continue;
|
|
2443
2536
|
}
|
|
2444
|
-
const relativePath = subDir ?
|
|
2537
|
+
const relativePath = subDir ? join7(subDir, finalName) : finalName;
|
|
2445
2538
|
uploaded.push(relativePath);
|
|
2446
2539
|
if (isAudioFile(finalName)) {
|
|
2447
2540
|
generateWaveformCache(projectDir, relativePath).catch(() => {
|
|
@@ -2456,7 +2549,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2456
2549
|
if ("error" in res) return res.error;
|
|
2457
2550
|
let fd;
|
|
2458
2551
|
try {
|
|
2459
|
-
fd =
|
|
2552
|
+
fd = openSync2(res.absPath, "r");
|
|
2460
2553
|
} catch {
|
|
2461
2554
|
if (c.req.query("optional") === "1") {
|
|
2462
2555
|
return c.json({ filename: res.filePath, content: "", missing: true });
|
|
@@ -2464,10 +2557,10 @@ function registerFileRoutes(api, adapter) {
|
|
|
2464
2557
|
return c.json({ error: "not found" }, 404);
|
|
2465
2558
|
}
|
|
2466
2559
|
try {
|
|
2467
|
-
if (!
|
|
2560
|
+
if (!fstatSync2(fd).isFile()) {
|
|
2468
2561
|
return c.json({ error: "not found", why: "not_a_file" }, 404);
|
|
2469
2562
|
}
|
|
2470
|
-
const content =
|
|
2563
|
+
const content = readFileSync6(fd);
|
|
2471
2564
|
const version = fileContentVersion(content);
|
|
2472
2565
|
c.header("ETag", version);
|
|
2473
2566
|
return c.json({
|
|
@@ -2477,7 +2570,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2477
2570
|
missing: false
|
|
2478
2571
|
});
|
|
2479
2572
|
} finally {
|
|
2480
|
-
|
|
2573
|
+
closeSync2(fd);
|
|
2481
2574
|
}
|
|
2482
2575
|
});
|
|
2483
2576
|
api.put("/projects/:id/files/*", async (c) => {
|
|
@@ -2489,7 +2582,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2489
2582
|
if (expectedVersion === null && !createOnly) {
|
|
2490
2583
|
let currentContent = null;
|
|
2491
2584
|
try {
|
|
2492
|
-
currentContent =
|
|
2585
|
+
currentContent = readFileSync6(res.absPath);
|
|
2493
2586
|
} catch (error) {
|
|
2494
2587
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2495
2588
|
throw error;
|
|
@@ -2510,12 +2603,12 @@ function registerFileRoutes(api, adapter) {
|
|
|
2510
2603
|
ensureDir(res.absPath);
|
|
2511
2604
|
let fd;
|
|
2512
2605
|
try {
|
|
2513
|
-
fd =
|
|
2606
|
+
fd = openSync2(res.absPath, "wx");
|
|
2514
2607
|
} catch (error) {
|
|
2515
2608
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
2516
2609
|
throw error;
|
|
2517
2610
|
}
|
|
2518
|
-
const currentContent =
|
|
2611
|
+
const currentContent = readFileSync6(res.absPath);
|
|
2519
2612
|
return c.json(
|
|
2520
2613
|
{
|
|
2521
2614
|
error: "file conflict",
|
|
@@ -2529,12 +2622,12 @@ function registerFileRoutes(api, adapter) {
|
|
|
2529
2622
|
try {
|
|
2530
2623
|
writeSync(fd, body, 0, body.length, 0);
|
|
2531
2624
|
} finally {
|
|
2532
|
-
|
|
2625
|
+
closeSync2(fd);
|
|
2533
2626
|
}
|
|
2534
2627
|
} else {
|
|
2535
2628
|
let fd;
|
|
2536
2629
|
try {
|
|
2537
|
-
fd =
|
|
2630
|
+
fd = openSync2(res.absPath, "r+");
|
|
2538
2631
|
} catch (error) {
|
|
2539
2632
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2540
2633
|
throw error;
|
|
@@ -2550,7 +2643,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2550
2643
|
);
|
|
2551
2644
|
}
|
|
2552
2645
|
try {
|
|
2553
|
-
const currentContent =
|
|
2646
|
+
const currentContent = readFileSync6(fd);
|
|
2554
2647
|
const currentVersion = fileContentVersion(currentContent);
|
|
2555
2648
|
if (expectedVersion !== currentVersion) {
|
|
2556
2649
|
return c.json(
|
|
@@ -2565,12 +2658,12 @@ function registerFileRoutes(api, adapter) {
|
|
|
2565
2658
|
}
|
|
2566
2659
|
backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
2567
2660
|
if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
|
|
2568
|
-
const mode =
|
|
2569
|
-
|
|
2661
|
+
const mode = fstatSync2(fd).mode;
|
|
2662
|
+
closeSync2(fd);
|
|
2570
2663
|
fd = null;
|
|
2571
2664
|
replaceFileAtomically(res.absPath, body, mode);
|
|
2572
2665
|
} finally {
|
|
2573
|
-
if (fd !== null)
|
|
2666
|
+
if (fd !== null) closeSync2(fd);
|
|
2574
2667
|
}
|
|
2575
2668
|
}
|
|
2576
2669
|
const version = fileContentVersion(body);
|
|
@@ -2591,7 +2684,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2591
2684
|
ensureDir(res.absPath);
|
|
2592
2685
|
const body = Buffer.from(await c.req.arrayBuffer());
|
|
2593
2686
|
try {
|
|
2594
|
-
|
|
2687
|
+
writeFileSync5(res.absPath, body, { flag: "wx" });
|
|
2595
2688
|
} catch (error) {
|
|
2596
2689
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
2597
2690
|
throw error;
|
|
@@ -2625,7 +2718,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2625
2718
|
}
|
|
2626
2719
|
let before;
|
|
2627
2720
|
try {
|
|
2628
|
-
before =
|
|
2721
|
+
before = readFileSync6(ctx.absPath, "utf-8");
|
|
2629
2722
|
} catch (error) {
|
|
2630
2723
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2631
2724
|
throw error;
|
|
@@ -2682,7 +2775,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2682
2775
|
}
|
|
2683
2776
|
const parsed = await parseMutationBody(c);
|
|
2684
2777
|
if ("error" in parsed) return parsed.error;
|
|
2685
|
-
const originalContent =
|
|
2778
|
+
const originalContent = readFileSync6(ctx.absPath, "utf-8");
|
|
2686
2779
|
return writeIfChanged(
|
|
2687
2780
|
c,
|
|
2688
2781
|
ctx.project.dir,
|
|
@@ -2703,7 +2796,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2703
2796
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
2704
2797
|
return c.json({ error: "targets required" }, 400);
|
|
2705
2798
|
}
|
|
2706
|
-
const originalContent =
|
|
2799
|
+
const originalContent = readFileSync6(ctx.absPath, "utf-8");
|
|
2707
2800
|
let next = originalContent;
|
|
2708
2801
|
for (const target of targets) {
|
|
2709
2802
|
next = removeElementFromHtml(next, target);
|
|
@@ -2736,7 +2829,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2736
2829
|
seen.add(absPath);
|
|
2737
2830
|
let before;
|
|
2738
2831
|
try {
|
|
2739
|
-
before =
|
|
2832
|
+
before = readFileSync6(absPath, "utf-8");
|
|
2740
2833
|
} catch {
|
|
2741
2834
|
return c.json({ error: `not found: ${file.path}` }, 404);
|
|
2742
2835
|
}
|
|
@@ -2763,7 +2856,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2763
2856
|
prepared.push(folded);
|
|
2764
2857
|
}
|
|
2765
2858
|
for (const file of prepared) {
|
|
2766
|
-
const current =
|
|
2859
|
+
const current = readFileSync6(file.absPath, "utf-8");
|
|
2767
2860
|
if (current !== file.before) {
|
|
2768
2861
|
return c.json(
|
|
2769
2862
|
{
|
|
@@ -2805,7 +2898,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2805
2898
|
const conflicts = [];
|
|
2806
2899
|
for (const file of written.reverse()) {
|
|
2807
2900
|
try {
|
|
2808
|
-
const current =
|
|
2901
|
+
const current = readFileSync6(file.absPath, "utf-8");
|
|
2809
2902
|
if (current !== file.after) {
|
|
2810
2903
|
conflicts.push(file.path);
|
|
2811
2904
|
continue;
|
|
@@ -2853,7 +2946,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2853
2946
|
const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
|
|
2854
2947
|
let originalContent;
|
|
2855
2948
|
try {
|
|
2856
|
-
originalContent =
|
|
2949
|
+
originalContent = readFileSync6(ctx.absPath, "utf-8");
|
|
2857
2950
|
} catch {
|
|
2858
2951
|
return c.json({ error: "not found" }, 404);
|
|
2859
2952
|
}
|
|
@@ -2909,7 +3002,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2909
3002
|
}
|
|
2910
3003
|
let originalContent;
|
|
2911
3004
|
try {
|
|
2912
|
-
originalContent =
|
|
3005
|
+
originalContent = readFileSync6(ctx.absPath, "utf-8");
|
|
2913
3006
|
} catch {
|
|
2914
3007
|
return c.json({ error: "not found" }, 404);
|
|
2915
3008
|
}
|
|
@@ -3010,7 +3103,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3010
3103
|
}
|
|
3011
3104
|
let originalContent;
|
|
3012
3105
|
try {
|
|
3013
|
-
originalContent =
|
|
3106
|
+
originalContent = readFileSync6(ctx.absPath, "utf-8");
|
|
3014
3107
|
} catch {
|
|
3015
3108
|
return c.json({ error: "not found" }, 404);
|
|
3016
3109
|
}
|
|
@@ -3065,7 +3158,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3065
3158
|
).map((entry) => ({ target: entry.target, track: entry.track }));
|
|
3066
3159
|
let originalContent;
|
|
3067
3160
|
try {
|
|
3068
|
-
originalContent =
|
|
3161
|
+
originalContent = readFileSync6(ctx.absPath, "utf-8");
|
|
3069
3162
|
} catch {
|
|
3070
3163
|
return c.json({ error: "not found" }, 404);
|
|
3071
3164
|
}
|
|
@@ -3094,7 +3187,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3094
3187
|
if ("error" in parsed) return parsed.error;
|
|
3095
3188
|
let content;
|
|
3096
3189
|
try {
|
|
3097
|
-
content =
|
|
3190
|
+
content = readFileSync6(ctx.absPath, "utf-8");
|
|
3098
3191
|
} catch {
|
|
3099
3192
|
return c.json({ exists: false });
|
|
3100
3193
|
}
|
|
@@ -3137,7 +3230,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3137
3230
|
return c.json({ error: "forbidden" }, 403);
|
|
3138
3231
|
}
|
|
3139
3232
|
ensureDir(destAbs);
|
|
3140
|
-
|
|
3233
|
+
writeFileSync5(destAbs, readFileSync6(srcAbs));
|
|
3141
3234
|
return c.json({ ok: true, path: copyPath }, 201);
|
|
3142
3235
|
});
|
|
3143
3236
|
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
@@ -3153,7 +3246,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3153
3246
|
const subDir = c.req.query("dir") ?? "";
|
|
3154
3247
|
const targetDir = subDir ? resolveWithinProject(project.dir, subDir) : project.dir;
|
|
3155
3248
|
if (!targetDir) return c.json({ error: "forbidden" }, 403);
|
|
3156
|
-
if (subDir && !existsSync3(targetDir))
|
|
3249
|
+
if (subDir && !existsSync3(targetDir)) mkdirSync4(targetDir, { recursive: true });
|
|
3157
3250
|
const formData = await c.req.formData();
|
|
3158
3251
|
const result = await processUploadedFiles(formData, targetDir, project.dir);
|
|
3159
3252
|
return c.json(
|
|
@@ -3167,7 +3260,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3167
3260
|
mustExist: true
|
|
3168
3261
|
});
|
|
3169
3262
|
if ("error" in res) return res.error;
|
|
3170
|
-
const html =
|
|
3263
|
+
const html = readFileSync6(res.absPath, "utf-8");
|
|
3171
3264
|
const block = extractGsapScriptBlock(html);
|
|
3172
3265
|
if (!block) {
|
|
3173
3266
|
return c.json({
|
|
@@ -3216,7 +3309,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3216
3309
|
if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
|
|
3217
3310
|
return c.json({ error: "expected and restore contents required" }, 400);
|
|
3218
3311
|
}
|
|
3219
|
-
const current =
|
|
3312
|
+
const current = readFileSync6(res.absPath, "utf-8");
|
|
3220
3313
|
if (current !== body.expected) {
|
|
3221
3314
|
return c.json({ ok: true, restored: false, conflict: true });
|
|
3222
3315
|
}
|
|
@@ -3226,10 +3319,10 @@ function registerFileRoutes(api, adapter) {
|
|
|
3226
3319
|
}
|
|
3227
3320
|
|
|
3228
3321
|
// src/routes/preview.ts
|
|
3229
|
-
import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync8, statSync as
|
|
3322
|
+
import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
|
|
3230
3323
|
import { Readable } from "stream";
|
|
3231
|
-
import { join as
|
|
3232
|
-
import { createHash as
|
|
3324
|
+
import { join as join9, resolve as resolve4 } from "path";
|
|
3325
|
+
import { createHash as createHash4 } from "crypto";
|
|
3233
3326
|
import {
|
|
3234
3327
|
injectScriptsIntoHtml,
|
|
3235
3328
|
stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2
|
|
@@ -3237,8 +3330,8 @@ import {
|
|
|
3237
3330
|
import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
3238
3331
|
|
|
3239
3332
|
// src/helpers/subComposition.ts
|
|
3240
|
-
import { existsSync as existsSync4, readFileSync as
|
|
3241
|
-
import { join as
|
|
3333
|
+
import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
|
|
3334
|
+
import { join as join8 } from "path";
|
|
3242
3335
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
3243
3336
|
import {
|
|
3244
3337
|
rewriteAssetPaths,
|
|
@@ -3250,7 +3343,7 @@ function isFullHtmlDocument(html) {
|
|
|
3250
3343
|
return /^\s*(?:<!doctype\s|<html[\s>])/i.test(html);
|
|
3251
3344
|
}
|
|
3252
3345
|
function rewriteRelativePaths(root, compPath, projectDir) {
|
|
3253
|
-
const assetExists = (path) => existsSync4(
|
|
3346
|
+
const assetExists = (path) => existsSync4(join8(projectDir, path));
|
|
3254
3347
|
rewriteAssetPaths(
|
|
3255
3348
|
root.querySelectorAll("[src], [href]"),
|
|
3256
3349
|
compPath,
|
|
@@ -3347,9 +3440,9 @@ function tagRootCompositionFile(bodyHtml, compPath) {
|
|
|
3347
3440
|
return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
|
|
3348
3441
|
}
|
|
3349
3442
|
function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
|
|
3350
|
-
const compFile =
|
|
3443
|
+
const compFile = join8(projectDir, compPath);
|
|
3351
3444
|
if (!existsSync4(compFile)) return null;
|
|
3352
|
-
const rawComp = rawOverride ??
|
|
3445
|
+
const rawComp = rawOverride ?? readFileSync7(compFile, "utf-8");
|
|
3353
3446
|
let compHeadContent = "";
|
|
3354
3447
|
let rewrittenContent;
|
|
3355
3448
|
let htmlAttrs = "";
|
|
@@ -3379,10 +3472,10 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
3379
3472
|
}
|
|
3380
3473
|
rewrittenContent = stripEmbeddedRuntimeScripts(rewrittenContent);
|
|
3381
3474
|
rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
|
|
3382
|
-
const indexPath =
|
|
3475
|
+
const indexPath = join8(projectDir, "index.html");
|
|
3383
3476
|
let headContent = "";
|
|
3384
3477
|
if (existsSync4(indexPath)) {
|
|
3385
|
-
const indexHtml =
|
|
3478
|
+
const indexHtml = readFileSync7(indexPath, "utf-8");
|
|
3386
3479
|
const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
3387
3480
|
headContent = headMatch?.[1] ?? "";
|
|
3388
3481
|
}
|
|
@@ -3419,63 +3512,7 @@ function hasBaseElement(html) {
|
|
|
3419
3512
|
}
|
|
3420
3513
|
|
|
3421
3514
|
// src/routes/preview.ts
|
|
3422
|
-
import { ensureHfIds as
|
|
3423
|
-
|
|
3424
|
-
// src/helpers/hfIdPersist.ts
|
|
3425
|
-
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
3426
|
-
import { closeSync as closeSync2, constants, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
|
|
3427
|
-
function persistHfIdsIfNeeded(filePath, html) {
|
|
3428
|
-
const normalized = ensureHfIds(html);
|
|
3429
|
-
const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3430
|
-
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3431
|
-
if (idsAfter > idsBefore) {
|
|
3432
|
-
try {
|
|
3433
|
-
const current = readFileSync7(filePath, "utf-8");
|
|
3434
|
-
if (current === html) {
|
|
3435
|
-
replaceFileAtomically(filePath, normalized, statSync4(filePath).mode);
|
|
3436
|
-
}
|
|
3437
|
-
} catch (err) {
|
|
3438
|
-
console.warn("[hyperframes] persistHfIdsIfNeeded: failed to write ids to disk:", err);
|
|
3439
|
-
}
|
|
3440
|
-
}
|
|
3441
|
-
return normalized;
|
|
3442
|
-
}
|
|
3443
|
-
function openNoFollow(filePath, flags) {
|
|
3444
|
-
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
3445
|
-
try {
|
|
3446
|
-
return openSync2(filePath, flags | noFollow);
|
|
3447
|
-
} catch {
|
|
3448
|
-
return null;
|
|
3449
|
-
}
|
|
3450
|
-
}
|
|
3451
|
-
function stampFileHfIds(filePath) {
|
|
3452
|
-
let fd = openNoFollow(filePath, constants.O_RDWR);
|
|
3453
|
-
let writable = true;
|
|
3454
|
-
if (fd === null) {
|
|
3455
|
-
fd = openNoFollow(filePath, constants.O_RDONLY);
|
|
3456
|
-
writable = false;
|
|
3457
|
-
}
|
|
3458
|
-
if (fd === null) return null;
|
|
3459
|
-
try {
|
|
3460
|
-
if (!fstatSync2(fd).isFile()) return null;
|
|
3461
|
-
const html = readFileSync7(fd, "utf-8");
|
|
3462
|
-
const normalized = ensureHfIds(html);
|
|
3463
|
-
const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3464
|
-
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3465
|
-
if (writable && idsAfter > idsBefore) {
|
|
3466
|
-
const mode = fstatSync2(fd).mode;
|
|
3467
|
-
closeSync2(fd);
|
|
3468
|
-
fd = null;
|
|
3469
|
-
replaceFileAtomically(filePath, normalized, mode);
|
|
3470
|
-
}
|
|
3471
|
-
return normalized;
|
|
3472
|
-
} catch (err) {
|
|
3473
|
-
console.warn("[hyperframes] stampFileHfIds: failed to stamp ids:", err);
|
|
3474
|
-
return null;
|
|
3475
|
-
} finally {
|
|
3476
|
-
if (fd !== null) closeSync2(fd);
|
|
3477
|
-
}
|
|
3478
|
-
}
|
|
3515
|
+
import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
|
|
3479
3516
|
|
|
3480
3517
|
// src/helpers/variablesPayload.ts
|
|
3481
3518
|
var VARIABLES_PAYLOAD_ERROR = "variables must be a JSON object of {variableId: value}";
|
|
@@ -3519,7 +3556,7 @@ function injectProjectSignature(html, signature) {
|
|
|
3519
3556
|
${html}`;
|
|
3520
3557
|
}
|
|
3521
3558
|
function readStudioMotionManifestContent(projectDir) {
|
|
3522
|
-
const manifestPath =
|
|
3559
|
+
const manifestPath = join9(projectDir, STUDIO_MOTION_PATH);
|
|
3523
3560
|
if (!existsSync5(manifestPath)) return "";
|
|
3524
3561
|
try {
|
|
3525
3562
|
return readFileSync8(manifestPath, "utf-8");
|
|
@@ -3633,7 +3670,7 @@ function parsePreviewVariablesParam(raw) {
|
|
|
3633
3670
|
}
|
|
3634
3671
|
function variablesEtagSalt(raw) {
|
|
3635
3672
|
if (!raw) return "";
|
|
3636
|
-
return `:vars:${
|
|
3673
|
+
return `:vars:${createHash4("sha1").update(raw).digest("hex").slice(0, 12)}`;
|
|
3637
3674
|
}
|
|
3638
3675
|
function previewVariablesFromRequest(rawVariables) {
|
|
3639
3676
|
const parse = parsePreviewVariablesParam(rawVariables);
|
|
@@ -3665,14 +3702,14 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
|
|
|
3665
3702
|
}
|
|
3666
3703
|
}
|
|
3667
3704
|
function resolveProjectMainHtml(projectDir, projectId) {
|
|
3668
|
-
const indexPath =
|
|
3705
|
+
const indexPath = join9(projectDir, "index.html");
|
|
3669
3706
|
if (existsSync5(indexPath)) {
|
|
3670
3707
|
return {
|
|
3671
3708
|
html: readFileSync8(indexPath, "utf-8"),
|
|
3672
3709
|
compositionPath: "index.html"
|
|
3673
3710
|
};
|
|
3674
3711
|
}
|
|
3675
|
-
const blockHtmlPath =
|
|
3712
|
+
const blockHtmlPath = join9(projectDir, `${projectId}.html`);
|
|
3676
3713
|
if (existsSync5(blockHtmlPath)) {
|
|
3677
3714
|
return {
|
|
3678
3715
|
html: readFileSync8(blockHtmlPath, "utf-8"),
|
|
@@ -3683,7 +3720,8 @@ function resolveProjectMainHtml(projectDir, projectId) {
|
|
|
3683
3720
|
}
|
|
3684
3721
|
var PREVIEW_BUNDLE_OPTIONS = {
|
|
3685
3722
|
runtime: "placeholder",
|
|
3686
|
-
inlineAssets: false
|
|
3723
|
+
inlineAssets: false,
|
|
3724
|
+
staticGuard: false
|
|
3687
3725
|
};
|
|
3688
3726
|
function registerPreviewRoutes(api, adapter) {
|
|
3689
3727
|
const previewCacheHeaders = (etag) => ({
|
|
@@ -3691,28 +3729,21 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3691
3729
|
ETag: etag
|
|
3692
3730
|
});
|
|
3693
3731
|
const mediaCodecProbeCache = resolvePreviewMediaCodecProbeCache(adapter);
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3703
|
-
if (ifNoneMatch === etag) {
|
|
3704
|
-
return new Response(null, {
|
|
3705
|
-
status: 304,
|
|
3706
|
-
headers: previewCacheHeaders(etag)
|
|
3707
|
-
});
|
|
3708
|
-
}
|
|
3732
|
+
const builtPreviews = /* @__PURE__ */ new Map();
|
|
3733
|
+
const rememberPreview = (key, html) => {
|
|
3734
|
+
builtPreviews.delete(key);
|
|
3735
|
+
builtPreviews.set(key, html);
|
|
3736
|
+
if (builtPreviews.size > 4) builtPreviews.delete(builtPreviews.keys().next().value);
|
|
3737
|
+
};
|
|
3738
|
+
const previewBuilds = /* @__PURE__ */ new Map();
|
|
3739
|
+
async function buildPreview(project, previewVariables, builtKey) {
|
|
3709
3740
|
const diskMain = resolveProjectMainHtml(project.dir, project.id);
|
|
3710
|
-
const normalizedDisk = diskMain ?
|
|
3741
|
+
const normalizedDisk = diskMain ? ensureHfIds3(diskMain.html) : null;
|
|
3711
3742
|
try {
|
|
3712
|
-
let bundled = await adapter.bundle(project.dir);
|
|
3743
|
+
let bundled = await adapter.bundle(project.dir, { stampHfIds: true });
|
|
3713
3744
|
let mainCompositionPath = "index.html";
|
|
3714
3745
|
if (!bundled) {
|
|
3715
|
-
if (!diskMain) return
|
|
3746
|
+
if (!diskMain) return null;
|
|
3716
3747
|
bundled = stripEmbeddedRuntimeScripts2(normalizedDisk ?? diskMain.html);
|
|
3717
3748
|
mainCompositionPath = diskMain.compositionPath;
|
|
3718
3749
|
}
|
|
@@ -3727,7 +3758,7 @@ ${runtimeTag}`;
|
|
|
3727
3758
|
bundled = bundled.replace(/<head>/i, `<head><base href="${baseHref}">`);
|
|
3728
3759
|
}
|
|
3729
3760
|
bundled = injectStudioPreviewAugmentations(
|
|
3730
|
-
|
|
3761
|
+
ensureHfIds3(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
|
|
3731
3762
|
adapter,
|
|
3732
3763
|
project.dir,
|
|
3733
3764
|
mainCompositionPath
|
|
@@ -3740,14 +3771,13 @@ ${runtimeTag}`;
|
|
|
3740
3771
|
mainCompositionPath,
|
|
3741
3772
|
mediaCodecProbeCache
|
|
3742
3773
|
);
|
|
3743
|
-
|
|
3774
|
+
rememberPreview(builtKey, bundled);
|
|
3775
|
+
adapter.previewDocuments?.write(builtKey, bundled);
|
|
3776
|
+
return bundled;
|
|
3744
3777
|
} catch {
|
|
3745
3778
|
const fallback = resolveProjectMainHtml(project.dir, project.id);
|
|
3746
3779
|
if (fallback) {
|
|
3747
|
-
const fallbackHtml =
|
|
3748
|
-
join8(project.dir, fallback.compositionPath),
|
|
3749
|
-
fallback.html
|
|
3750
|
-
);
|
|
3780
|
+
const fallbackHtml = ensureHfIds3(fallback.html);
|
|
3751
3781
|
let fallbackAugmented = injectStudioPreviewAugmentations(
|
|
3752
3782
|
await transformPreviewHtml(fallbackHtml, adapter, project, fallback.compositionPath),
|
|
3753
3783
|
adapter,
|
|
@@ -3764,14 +3794,51 @@ ${runtimeTag}`;
|
|
|
3764
3794
|
fallback.compositionPath,
|
|
3765
3795
|
mediaCodecProbeCache
|
|
3766
3796
|
);
|
|
3767
|
-
return
|
|
3797
|
+
return fallbackAugmented;
|
|
3768
3798
|
}
|
|
3769
|
-
return
|
|
3799
|
+
return null;
|
|
3770
3800
|
}
|
|
3801
|
+
}
|
|
3802
|
+
api.get("/projects/:id/preview", async (c) => {
|
|
3803
|
+
const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
|
|
3804
|
+
if (!resolved) return c.json({ error: "not found" }, 404);
|
|
3805
|
+
const { project, signature } = resolved;
|
|
3806
|
+
const vars = previewVariablesFromRequest(c.req.query("variables"));
|
|
3807
|
+
if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
|
|
3808
|
+
const previewVariables = vars.values;
|
|
3809
|
+
const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`;
|
|
3810
|
+
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3811
|
+
if (ifNoneMatch === etag) {
|
|
3812
|
+
return new Response(null, {
|
|
3813
|
+
status: 304,
|
|
3814
|
+
headers: previewCacheHeaders(etag)
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
const builtKey = `${project.id}
|
|
3818
|
+
${etag}`;
|
|
3819
|
+
const cached = builtPreviews.get(builtKey) ?? adapter.previewDocuments?.read(builtKey);
|
|
3820
|
+
if (cached) {
|
|
3821
|
+
rememberPreview(builtKey, cached);
|
|
3822
|
+
return c.html(cached, 200, previewCacheHeaders(etag));
|
|
3823
|
+
}
|
|
3824
|
+
let pending = previewBuilds.get(builtKey);
|
|
3825
|
+
if (!pending) {
|
|
3826
|
+
pending = buildPreview(project, previewVariables, builtKey).finally(
|
|
3827
|
+
() => previewBuilds.delete(builtKey)
|
|
3828
|
+
);
|
|
3829
|
+
previewBuilds.set(builtKey, pending);
|
|
3830
|
+
}
|
|
3831
|
+
const html = await pending;
|
|
3832
|
+
if (!html) return c.text("not found", 404);
|
|
3833
|
+
return c.html(html, 200, previewCacheHeaders(etag));
|
|
3771
3834
|
});
|
|
3772
3835
|
function pinSubCompHfIds(compFile, compPath) {
|
|
3773
3836
|
if (!/\.html?$/i.test(compPath)) return void 0;
|
|
3774
|
-
|
|
3837
|
+
try {
|
|
3838
|
+
return ensureHfIds3(readFileSync8(compFile, "utf-8"));
|
|
3839
|
+
} catch {
|
|
3840
|
+
return null;
|
|
3841
|
+
}
|
|
3775
3842
|
}
|
|
3776
3843
|
api.get("/projects/:id/preview/comp/*", async (c) => {
|
|
3777
3844
|
const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
|
|
@@ -3782,10 +3849,10 @@ ${runtimeTag}`;
|
|
|
3782
3849
|
const previewVariables = vars.values;
|
|
3783
3850
|
const compPath = requestSubPath(c.req.url, "projects/:id/preview/comp");
|
|
3784
3851
|
const compFile = resolveWithinProject(project.dir, compPath);
|
|
3785
|
-
if (!compFile || !existsSync5(compFile) || !
|
|
3852
|
+
if (!compFile || !existsSync5(compFile) || !statSync4(compFile).isFile()) {
|
|
3786
3853
|
return c.text("not found", 404);
|
|
3787
3854
|
}
|
|
3788
|
-
const compPathHash =
|
|
3855
|
+
const compPathHash = createHash4("sha1").update(compPath).digest("hex");
|
|
3789
3856
|
const etag = `"comp:v2:${compPathHash}:${signature}${variablesEtagSalt(vars.raw)}"`;
|
|
3790
3857
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3791
3858
|
if (ifNoneMatch === etag) {
|
|
@@ -3805,7 +3872,7 @@ ${runtimeTag}`;
|
|
|
3805
3872
|
stamped
|
|
3806
3873
|
);
|
|
3807
3874
|
if (!html) return c.text("not found", 404);
|
|
3808
|
-
html =
|
|
3875
|
+
html = ensureHfIds3(await transformPreviewHtml(html, adapter, project, compPath));
|
|
3809
3876
|
html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath);
|
|
3810
3877
|
if (previewVariables) html = injectPreviewVariables(html, previewVariables);
|
|
3811
3878
|
html = await injectMediaCodecMap(html, adapter, project.dir, compPath, mediaCodecProbeCache);
|
|
@@ -3820,7 +3887,7 @@ ${runtimeTag}`;
|
|
|
3820
3887
|
if (!file) {
|
|
3821
3888
|
return c.text("not found", 404);
|
|
3822
3889
|
}
|
|
3823
|
-
const stat2 = existsSync5(file) ?
|
|
3890
|
+
const stat2 = existsSync5(file) ? statSync4(file) : null;
|
|
3824
3891
|
if (!stat2?.isFile()) {
|
|
3825
3892
|
return c.text("not found", 404);
|
|
3826
3893
|
}
|
|
@@ -3868,7 +3935,7 @@ ${runtimeTag}`;
|
|
|
3868
3935
|
servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
|
|
3869
3936
|
}
|
|
3870
3937
|
const textBuffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : null;
|
|
3871
|
-
const totalSize = textBuffer ? textBuffer.length :
|
|
3938
|
+
const totalSize = textBuffer ? textBuffer.length : statSync4(servedPath).size;
|
|
3872
3939
|
const bodyFor = (start, end) => textBuffer ? new Uint8Array(textBuffer.subarray(start, end + 1)) : (
|
|
3873
3940
|
// Node's web stream type and the DOM one do not overlap for tsc on
|
|
3874
3941
|
// every platform's lib set; the double cast is the documented bridge.
|
|
@@ -3912,7 +3979,7 @@ ${runtimeTag}`;
|
|
|
3912
3979
|
|
|
3913
3980
|
// src/routes/lint.ts
|
|
3914
3981
|
import { readFileSync as readFileSync9 } from "fs";
|
|
3915
|
-
import { join as
|
|
3982
|
+
import { join as join10 } from "path";
|
|
3916
3983
|
async function collectProjectFindings(adapter, projectDir) {
|
|
3917
3984
|
const findings = [];
|
|
3918
3985
|
const coveredFiles = /* @__PURE__ */ new Set();
|
|
@@ -3930,7 +3997,7 @@ async function lintUncoveredHtml(adapter, projectDir, htmlFiles, coveredFiles) {
|
|
|
3930
3997
|
const findings = [];
|
|
3931
3998
|
for (const file of htmlFiles) {
|
|
3932
3999
|
if (coveredFiles.has(file)) continue;
|
|
3933
|
-
const content = readFileSync9(
|
|
4000
|
+
const content = readFileSync9(join10(projectDir, file), "utf-8");
|
|
3934
4001
|
const result = await adapter.lint(content, { filePath: file, isSubComposition: true });
|
|
3935
4002
|
for (const finding of result?.findings ?? []) {
|
|
3936
4003
|
findings.push({ ...finding, file });
|
|
@@ -3963,8 +4030,8 @@ function registerLintRoutes(api, adapter) {
|
|
|
3963
4030
|
|
|
3964
4031
|
// src/routes/render.ts
|
|
3965
4032
|
import { streamSSE } from "hono/streaming";
|
|
3966
|
-
import { existsSync as existsSync6, readFileSync as readFileSync10, mkdirSync as
|
|
3967
|
-
import { join as
|
|
4033
|
+
import { existsSync as existsSync6, readFileSync as readFileSync10, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
4034
|
+
import { join as join11 } from "path";
|
|
3968
4035
|
import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
|
|
3969
4036
|
import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
|
|
3970
4037
|
var VALID_RESOLUTIONS = new Set(VALID_CANVAS_RESOLUTIONS);
|
|
@@ -4030,9 +4097,9 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4030
4097
|
const now = /* @__PURE__ */ new Date();
|
|
4031
4098
|
const jobId = `${project.id}_${formatRenderOutputTimestamp(now)}`;
|
|
4032
4099
|
const rendersDir = adapter.rendersDir(project);
|
|
4033
|
-
if (!existsSync6(rendersDir))
|
|
4100
|
+
if (!existsSync6(rendersDir)) mkdirSync5(rendersDir, { recursive: true });
|
|
4034
4101
|
const ext = FORMAT_EXT[format] ?? ".mp4";
|
|
4035
|
-
const outputPath =
|
|
4102
|
+
const outputPath = join11(rendersDir, `${jobId}${ext}`);
|
|
4036
4103
|
const jobState = adapter.startRender({
|
|
4037
4104
|
project,
|
|
4038
4105
|
outputPath,
|
|
@@ -4133,7 +4200,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4133
4200
|
if (state.id === jobId && state.outputPath) {
|
|
4134
4201
|
const dir = state.outputPath.replace(/\/[^/]+$/, "");
|
|
4135
4202
|
for (const ext of [".mp4", ".webm", ".mov", ".meta.json"]) {
|
|
4136
|
-
const fp =
|
|
4203
|
+
const fp = join11(dir, `${jobId}${ext}`);
|
|
4137
4204
|
if (existsSync6(fp)) unlinkSync3(fp);
|
|
4138
4205
|
}
|
|
4139
4206
|
break;
|
|
@@ -4168,10 +4235,10 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4168
4235
|
const rendersDir = adapter.rendersDir(project);
|
|
4169
4236
|
if (!existsSync6(rendersDir)) return c.json({ renders: [] });
|
|
4170
4237
|
const files = readdirSync5(rendersDir).filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov")).map((f) => {
|
|
4171
|
-
const fp =
|
|
4172
|
-
const stat2 =
|
|
4238
|
+
const fp = join11(rendersDir, f);
|
|
4239
|
+
const stat2 = statSync5(fp);
|
|
4173
4240
|
const rid = f.replace(/\.(mp4|webm|mov)$/, "");
|
|
4174
|
-
const metaPath =
|
|
4241
|
+
const metaPath = join11(rendersDir, `${rid}.meta.json`);
|
|
4175
4242
|
let status = "complete";
|
|
4176
4243
|
let durationMs;
|
|
4177
4244
|
if (existsSync6(metaPath)) {
|
|
@@ -4197,7 +4264,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4197
4264
|
id: file.id,
|
|
4198
4265
|
status: file.status,
|
|
4199
4266
|
progress: 100,
|
|
4200
|
-
outputPath:
|
|
4267
|
+
outputPath: join11(rendersDir, file.filename),
|
|
4201
4268
|
createdAt: file.createdAt
|
|
4202
4269
|
});
|
|
4203
4270
|
}
|
|
@@ -4212,17 +4279,17 @@ import {
|
|
|
4212
4279
|
existsSync as existsSync7,
|
|
4213
4280
|
fstatSync as fstatSync3,
|
|
4214
4281
|
openSync as openSync3,
|
|
4215
|
-
mkdirSync as
|
|
4282
|
+
mkdirSync as mkdirSync6,
|
|
4216
4283
|
readFileSync as readFileSync12,
|
|
4217
4284
|
readdirSync as readdirSync6,
|
|
4218
4285
|
renameSync as renameSync3,
|
|
4219
4286
|
rmSync as rmSync4,
|
|
4220
|
-
statSync as
|
|
4287
|
+
statSync as statSync6,
|
|
4221
4288
|
unlinkSync as unlinkSync4,
|
|
4222
|
-
writeFileSync as
|
|
4289
|
+
writeFileSync as writeFileSync6
|
|
4223
4290
|
} from "fs";
|
|
4224
|
-
import { join as
|
|
4225
|
-
import { createHash as
|
|
4291
|
+
import { join as join12 } from "path";
|
|
4292
|
+
import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
|
|
4226
4293
|
|
|
4227
4294
|
// src/helpers/compositionInputs.ts
|
|
4228
4295
|
import { readFileSync as readFileSync11 } from "fs";
|
|
@@ -4395,9 +4462,9 @@ function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
|
|
|
4395
4462
|
if (!existsSync7(cacheDir)) return;
|
|
4396
4463
|
const files = readdirSync6(cacheDir, { withFileTypes: true }).flatMap((entry) => {
|
|
4397
4464
|
if (!entry.isFile()) return [];
|
|
4398
|
-
const path =
|
|
4465
|
+
const path = join12(cacheDir, entry.name);
|
|
4399
4466
|
try {
|
|
4400
|
-
const stats =
|
|
4467
|
+
const stats = statSync6(path);
|
|
4401
4468
|
return [{ path, bytes: stats.size, mtimeMs: stats.mtimeMs }];
|
|
4402
4469
|
} catch {
|
|
4403
4470
|
return [];
|
|
@@ -4425,7 +4492,7 @@ function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
|
|
|
4425
4492
|
function writeThumbnailAtomically(path, buffer) {
|
|
4426
4493
|
const temporaryPath = `${path}.${process.pid}.${randomUUID4()}.tmp`;
|
|
4427
4494
|
try {
|
|
4428
|
-
|
|
4495
|
+
writeFileSync6(temporaryPath, buffer, { flag: "wx" });
|
|
4429
4496
|
renameSync3(temporaryPath, path);
|
|
4430
4497
|
} finally {
|
|
4431
4498
|
rmSync4(temporaryPath, { force: true });
|
|
@@ -4481,7 +4548,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4481
4548
|
let sourceKey = "";
|
|
4482
4549
|
if (source !== "missing") {
|
|
4483
4550
|
const { html } = source;
|
|
4484
|
-
sourceKey = `_${
|
|
4551
|
+
sourceKey = `_${createHash5("sha1").update(html).digest("hex").slice(0, 16)}`;
|
|
4485
4552
|
sourceMtime = Math.round(source.mtimeMs);
|
|
4486
4553
|
if (!vpWidth) {
|
|
4487
4554
|
const wMatch = html.match(/data-width=["'](\d+)["']/);
|
|
@@ -4490,31 +4557,31 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4490
4557
|
if (hMatch?.[1]) compH = parseInt(hMatch[1]);
|
|
4491
4558
|
}
|
|
4492
4559
|
}
|
|
4493
|
-
const manualEditsFile =
|
|
4560
|
+
const manualEditsFile = join12(project.dir, STUDIO_MANUAL_EDITS_PATH);
|
|
4494
4561
|
let manualEditsKey = "";
|
|
4495
4562
|
if (existsSync7(manualEditsFile)) {
|
|
4496
4563
|
const manualEditsContent = readFileSync12(manualEditsFile, "utf-8");
|
|
4497
|
-
manualEditsKey = `_${
|
|
4498
|
-
sourceMtime = Math.max(sourceMtime, Math.round(
|
|
4564
|
+
manualEditsKey = `_${createHash5("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
|
|
4565
|
+
sourceMtime = Math.max(sourceMtime, Math.round(statSync6(manualEditsFile).mtimeMs));
|
|
4499
4566
|
}
|
|
4500
|
-
const motionFile =
|
|
4567
|
+
const motionFile = join12(project.dir, STUDIO_MOTION_PATH);
|
|
4501
4568
|
let motionKey = "";
|
|
4502
4569
|
if (existsSync7(motionFile)) {
|
|
4503
4570
|
const motionContent = readFileSync12(motionFile, "utf-8");
|
|
4504
|
-
motionKey = `_${
|
|
4505
|
-
sourceMtime = Math.max(sourceMtime, Math.round(
|
|
4571
|
+
motionKey = `_${createHash5("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
|
|
4572
|
+
sourceMtime = Math.max(sourceMtime, Math.round(statSync6(motionFile).mtimeMs));
|
|
4506
4573
|
}
|
|
4507
4574
|
const projectUrl = `http://${c.req.header("host")}/api/projects/${encodeURIComponent(project.id)}`;
|
|
4508
4575
|
const previewUrl = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
|
|
4509
|
-
const cacheDir =
|
|
4576
|
+
const cacheDir = join12(project.dir, ".thumbnails");
|
|
4510
4577
|
const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
|
|
4511
4578
|
const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
|
|
4512
|
-
const inputSignatureKey = `_${
|
|
4579
|
+
const inputSignatureKey = `_${createHash5("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
|
|
4513
4580
|
const outputScale = outputMode === "source" ? 1 : Math.min(1, THUMBNAIL_MAX_OUTPUT_WIDTH / compW, THUMBNAIL_MAX_OUTPUT_HEIGHT / compH);
|
|
4514
4581
|
const outputWidth = Math.max(1, Math.round(compW * outputScale));
|
|
4515
4582
|
const outputHeight = Math.max(1, Math.round(compH * outputScale));
|
|
4516
4583
|
const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${inputSignatureKey}${manualEditsKey}${motionKey}${sourceKey}_${format}_${outputMode}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${outputWidth}x${outputHeight}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
|
|
4517
|
-
const cachePath =
|
|
4584
|
+
const cachePath = join12(cacheDir, cacheKey);
|
|
4518
4585
|
if (!prunedCacheDirs.has(cacheDir)) {
|
|
4519
4586
|
prunedCacheDirs.add(cacheDir);
|
|
4520
4587
|
pruneThumbnailCache(
|
|
@@ -4527,6 +4594,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4527
4594
|
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
4528
4595
|
});
|
|
4529
4596
|
}
|
|
4597
|
+
if (url.searchParams.get("cached") === "1") return c.body(null, 404);
|
|
4530
4598
|
try {
|
|
4531
4599
|
const buffer = await thumbnailGenerationCoordinator.acquire(
|
|
4532
4600
|
cachePath,
|
|
@@ -4552,7 +4620,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4552
4620
|
if (afterGeneration?.project.dir !== project.dir || !inputsUnchanged(afterGeneration.signature) || !inputsUnchanged(createProjectSignature(project.dir))) {
|
|
4553
4621
|
return generated;
|
|
4554
4622
|
}
|
|
4555
|
-
if (!existsSync7(cacheDir))
|
|
4623
|
+
if (!existsSync7(cacheDir)) mkdirSync6(cacheDir, { recursive: true });
|
|
4556
4624
|
writeThumbnailAtomically(cachePath, generated);
|
|
4557
4625
|
return generated;
|
|
4558
4626
|
}
|
|
@@ -4578,8 +4646,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4578
4646
|
}
|
|
4579
4647
|
|
|
4580
4648
|
// src/routes/waveform.ts
|
|
4581
|
-
import { existsSync as existsSync8, readFileSync as readFileSync13, statSync as
|
|
4582
|
-
import { join as
|
|
4649
|
+
import { existsSync as existsSync8, readFileSync as readFileSync13, statSync as statSync7 } from "fs";
|
|
4650
|
+
import { join as join13, resolve as resolve5 } from "path";
|
|
4583
4651
|
import { isWithinProjectRoot as isWithinProjectRoot2 } from "@hyperframes/parsers/asset-resolution";
|
|
4584
4652
|
function registerWaveformRoutes(api, adapter) {
|
|
4585
4653
|
api.get("/projects/:id/waveform/*", async (c) => {
|
|
@@ -4590,10 +4658,10 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4590
4658
|
if (!isWithinProjectRoot2(project.dir, audioPath)) {
|
|
4591
4659
|
return c.json({ error: "file not found" }, 404);
|
|
4592
4660
|
}
|
|
4593
|
-
const stats =
|
|
4661
|
+
const stats = statSync7(audioPath, { throwIfNoEntry: false });
|
|
4594
4662
|
if (!stats) return c.json({ error: "file not found" }, 404);
|
|
4595
|
-
const cacheDir =
|
|
4596
|
-
const cachePath =
|
|
4663
|
+
const cacheDir = join13(project.dir, ".waveform-cache");
|
|
4664
|
+
const cachePath = join13(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
4597
4665
|
try {
|
|
4598
4666
|
if (isWaveformCacheDirectory(cacheDir) && existsSync8(cachePath)) {
|
|
4599
4667
|
const peaks2 = JSON.parse(readFileSync13(cachePath, "utf-8"));
|
|
@@ -4879,8 +4947,8 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
4879
4947
|
|
|
4880
4948
|
// src/routes/media.ts
|
|
4881
4949
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
4882
|
-
import { existsSync as existsSync9, mkdirSync as
|
|
4883
|
-
import { basename as basename2, dirname as
|
|
4950
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7 } from "fs";
|
|
4951
|
+
import { basename as basename2, dirname as dirname5, extname as extname2, join as join14 } from "path";
|
|
4884
4952
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4885
4953
|
".mp4",
|
|
4886
4954
|
".mov",
|
|
@@ -4917,7 +4985,7 @@ function uniqueAssetPath(projectDir, assetPath) {
|
|
|
4917
4985
|
const ext = extname2(assetPath);
|
|
4918
4986
|
const withoutExt = assetPath.slice(0, -ext.length);
|
|
4919
4987
|
let candidate = assetPath;
|
|
4920
|
-
for (let index = 2; existsSync9(
|
|
4988
|
+
for (let index = 2; existsSync9(join14(projectDir, candidate)); index++) {
|
|
4921
4989
|
candidate = `${withoutExt}-${index}${ext}`;
|
|
4922
4990
|
}
|
|
4923
4991
|
return candidate;
|
|
@@ -5024,8 +5092,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5024
5092
|
return c.json({ error: "forbidden" }, 403);
|
|
5025
5093
|
}
|
|
5026
5094
|
}
|
|
5027
|
-
|
|
5028
|
-
if (backgroundOutputPath)
|
|
5095
|
+
mkdirSync7(dirname5(outputPath), { recursive: true });
|
|
5096
|
+
if (backgroundOutputPath) mkdirSync7(dirname5(backgroundOutputPath), { recursive: true });
|
|
5029
5097
|
const jobId = makeJobId(project.id, mediaJobs);
|
|
5030
5098
|
const state = adapter.startBackgroundRemoval({
|
|
5031
5099
|
project,
|
|
@@ -5084,9 +5152,9 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5084
5152
|
// src/routes/globalAssets.ts
|
|
5085
5153
|
import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
|
|
5086
5154
|
import { homedir } from "os";
|
|
5087
|
-
import { join as
|
|
5155
|
+
import { join as join15 } from "path";
|
|
5088
5156
|
function readGlobalAssets(home = homedir()) {
|
|
5089
|
-
const manifestPath =
|
|
5157
|
+
const manifestPath = join15(home, ".media", "manifest.jsonl");
|
|
5090
5158
|
if (!existsSync10(manifestPath)) return [];
|
|
5091
5159
|
const out = [];
|
|
5092
5160
|
for (const line of readFileSync14(manifestPath, "utf8").split("\n")) {
|
|
@@ -5108,6 +5176,7 @@ function registerGlobalAssetRoutes(api) {
|
|
|
5108
5176
|
|
|
5109
5177
|
// src/routes/history.ts
|
|
5110
5178
|
var YOU = { kind: "person", name: "You" };
|
|
5179
|
+
var MAX_WINDOW_IDLE_MS = 10 * 6e4;
|
|
5111
5180
|
async function historyOf(adapter, c) {
|
|
5112
5181
|
const project = await adapter.resolveProject(c.req.param("id") ?? "");
|
|
5113
5182
|
return project ? await adapter.history?.(project) ?? null : null;
|
|
@@ -5117,6 +5186,27 @@ async function bodyOf(c) {
|
|
|
5117
5186
|
return body && typeof body === "object" ? body : {};
|
|
5118
5187
|
}
|
|
5119
5188
|
var text = (value) => typeof value === "string" && value ? value : null;
|
|
5189
|
+
function writing(c) {
|
|
5190
|
+
const header = c.req.header("X-Hyperframes-Write-Token");
|
|
5191
|
+
return header ? { writeToken: createWriteToken(header) } : {};
|
|
5192
|
+
}
|
|
5193
|
+
function idleOf(body) {
|
|
5194
|
+
const idleMs = body.idleMs;
|
|
5195
|
+
return typeof idleMs === "number" && idleMs > 0 ? Math.min(idleMs, MAX_WINDOW_IDLE_MS) : void 0;
|
|
5196
|
+
}
|
|
5197
|
+
function versionsOf(value) {
|
|
5198
|
+
if (!value || typeof value !== "object") return void 0;
|
|
5199
|
+
const pairs = Object.entries(value).filter(
|
|
5200
|
+
(pair) => typeof pair[1] === "string"
|
|
5201
|
+
);
|
|
5202
|
+
return pairs.length ? Object.fromEntries(pairs) : void 0;
|
|
5203
|
+
}
|
|
5204
|
+
function nextStep(history, direction) {
|
|
5205
|
+
const target = history.next(direction);
|
|
5206
|
+
if (!target) return null;
|
|
5207
|
+
const paths = target.files.map((file) => file.path);
|
|
5208
|
+
return { id: target.id, label: target.label, endedAt: target.endedAt, paths };
|
|
5209
|
+
}
|
|
5120
5210
|
async function withHistory(adapter, c, task) {
|
|
5121
5211
|
const history = await historyOf(adapter, c);
|
|
5122
5212
|
if (!history) return c.json({ error: "This project has no history here." }, 404);
|
|
@@ -5129,25 +5219,35 @@ async function withHistory(adapter, c, task) {
|
|
|
5129
5219
|
function registerHistoryRoutes(api, adapter) {
|
|
5130
5220
|
const windows = /* @__PURE__ */ new Map();
|
|
5131
5221
|
const base = "/projects/:id/history";
|
|
5132
|
-
api.get(
|
|
5222
|
+
api.get(
|
|
5223
|
+
base,
|
|
5224
|
+
(c) => withHistory(adapter, c, (history) => {
|
|
5225
|
+
const entries = history.list();
|
|
5226
|
+
return { entries, back: nextStep(history, "back"), forward: nextStep(history, "forward") };
|
|
5227
|
+
})
|
|
5228
|
+
);
|
|
5133
5229
|
api.post(
|
|
5134
5230
|
`${base}/step`,
|
|
5135
5231
|
(c) => withHistory(
|
|
5136
5232
|
adapter,
|
|
5137
5233
|
c,
|
|
5138
|
-
(history, body) => history.step(body.direction === "forward" ? "forward" : "back", YOU)
|
|
5234
|
+
(history, body) => history.step(body.direction === "forward" ? "forward" : "back", YOU, writing(c))
|
|
5139
5235
|
)
|
|
5140
5236
|
);
|
|
5141
5237
|
api.post(
|
|
5142
5238
|
`${base}/undo`,
|
|
5143
5239
|
(c) => withHistory(adapter, c, (history, body) => {
|
|
5144
5240
|
const mode = body.mode === "just-this" || body.mode === "back-to-before" ? body.mode : void 0;
|
|
5145
|
-
return history.undo(text(body.entryId) ?? "", { who: YOU, mode });
|
|
5241
|
+
return history.undo(text(body.entryId) ?? "", { who: YOU, mode, ...writing(c) });
|
|
5146
5242
|
})
|
|
5147
5243
|
);
|
|
5148
5244
|
api.post(
|
|
5149
5245
|
`${base}/restore`,
|
|
5150
|
-
(c) => withHistory(
|
|
5246
|
+
(c) => withHistory(
|
|
5247
|
+
adapter,
|
|
5248
|
+
c,
|
|
5249
|
+
(history, body) => history.restore(text(body.point) ?? "", YOU, writing(c))
|
|
5250
|
+
)
|
|
5151
5251
|
);
|
|
5152
5252
|
api.get(
|
|
5153
5253
|
`${base}/peek/:point`,
|
|
@@ -5160,10 +5260,30 @@ function registerHistoryRoutes(api, adapter) {
|
|
|
5160
5260
|
return { ok: true };
|
|
5161
5261
|
})
|
|
5162
5262
|
);
|
|
5263
|
+
api.post(
|
|
5264
|
+
`${base}/claim`,
|
|
5265
|
+
(c) => withHistory(adapter, c, async (history, body) => {
|
|
5266
|
+
const paths = Array.isArray(body.paths) ? body.paths.filter((path) => text(path)) : [];
|
|
5267
|
+
const coalesceKey = text(body.coalesceKey) ?? void 0;
|
|
5268
|
+
const idleMs = idleOf(body);
|
|
5269
|
+
const overwrote = versionsOf(body.overwrote);
|
|
5270
|
+
const claimed = await history.claim(YOU, text(body.label) ?? "Edited in Studio", paths, {
|
|
5271
|
+
...coalesceKey && { coalesceKey },
|
|
5272
|
+
...idleMs && { idleMs },
|
|
5273
|
+
...overwrote && { overwrote }
|
|
5274
|
+
});
|
|
5275
|
+
return { claimed };
|
|
5276
|
+
})
|
|
5277
|
+
);
|
|
5163
5278
|
api.post(
|
|
5164
5279
|
`${base}/window`,
|
|
5165
5280
|
(c) => withHistory(adapter, c, async (history, body) => {
|
|
5166
|
-
const
|
|
5281
|
+
const idleMs = idleOf(body);
|
|
5282
|
+
const window = await history.beginWindow(
|
|
5283
|
+
YOU,
|
|
5284
|
+
text(body.label) ?? "Edited in Studio",
|
|
5285
|
+
idleMs ? { idleMs } : void 0
|
|
5286
|
+
);
|
|
5167
5287
|
windows.set(window.id, { history, window });
|
|
5168
5288
|
return { windowId: window.id };
|
|
5169
5289
|
})
|
|
@@ -5202,21 +5322,23 @@ function createStudioApi(adapter) {
|
|
|
5202
5322
|
// src/history/projectHistory.ts
|
|
5203
5323
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
5204
5324
|
import { rm as rm2 } from "fs/promises";
|
|
5205
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
5206
|
-
import {
|
|
5325
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync17 } from "fs";
|
|
5326
|
+
import { homedir as homedir2 } from "os";
|
|
5327
|
+
import { join as join18, relative as relative4, resolve as resolve7, sep as sep3 } from "path";
|
|
5207
5328
|
|
|
5208
5329
|
// src/history/blobStore.ts
|
|
5209
|
-
import { createHash as
|
|
5330
|
+
import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
|
|
5210
5331
|
import { constants as constants3, createReadStream as createReadStream2 } from "fs";
|
|
5211
5332
|
import { copyFile, mkdir, readdir, readFile as readFile2, rename, rm, stat } from "fs/promises";
|
|
5212
|
-
import { dirname as
|
|
5333
|
+
import { dirname as dirname6, join as join16 } from "path";
|
|
5334
|
+
var BLOB_HASH = /^[0-9a-f]{64}$/;
|
|
5213
5335
|
async function hashFile(path) {
|
|
5214
|
-
const hash =
|
|
5336
|
+
const hash = createHash6("sha256");
|
|
5215
5337
|
for await (const chunk of createReadStream2(path)) hash.update(chunk);
|
|
5216
5338
|
return hash.digest("hex");
|
|
5217
5339
|
}
|
|
5218
5340
|
async function cloneOrCopy(from, to) {
|
|
5219
|
-
await mkdir(
|
|
5341
|
+
await mkdir(dirname6(to), { recursive: true });
|
|
5220
5342
|
const temp = `${to}.${randomUUID5()}.tmp`;
|
|
5221
5343
|
try {
|
|
5222
5344
|
await copyFile(from, temp, constants3.COPYFILE_FICLONE);
|
|
@@ -5230,13 +5352,16 @@ async function openBlobStore(dir) {
|
|
|
5230
5352
|
const sizes = /* @__PURE__ */ new Map();
|
|
5231
5353
|
await mkdir(dir, { recursive: true });
|
|
5232
5354
|
for (const shard of await readdir(dir))
|
|
5233
|
-
for (const hash of await readdir(
|
|
5234
|
-
sizes.set(hash, (await stat(
|
|
5235
|
-
const pathOf = (hash) =>
|
|
5355
|
+
for (const hash of await readdir(join16(dir, shard)).catch(() => []))
|
|
5356
|
+
sizes.set(hash, (await stat(join16(dir, shard, hash))).size);
|
|
5357
|
+
const pathOf = (hash) => {
|
|
5358
|
+
if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
|
|
5359
|
+
return join16(dir, hash.slice(0, 2), hash);
|
|
5360
|
+
};
|
|
5236
5361
|
let total = [...sizes.values()].reduce((sum, size) => sum + size, 0);
|
|
5237
5362
|
return {
|
|
5238
5363
|
async put(absPath) {
|
|
5239
|
-
const temp =
|
|
5364
|
+
const temp = join16(dir, `incoming-${randomUUID5()}`);
|
|
5240
5365
|
await mkdir(dir, { recursive: true });
|
|
5241
5366
|
await copyFile(absPath, temp, constants3.COPYFILE_FICLONE);
|
|
5242
5367
|
const hash = await hashFile(temp);
|
|
@@ -5244,15 +5369,16 @@ async function openBlobStore(dir) {
|
|
|
5244
5369
|
await rm(temp, { force: true });
|
|
5245
5370
|
return hash;
|
|
5246
5371
|
}
|
|
5247
|
-
await mkdir(
|
|
5372
|
+
await mkdir(dirname6(pathOf(hash)), { recursive: true });
|
|
5248
5373
|
const size = (await stat(temp)).size;
|
|
5249
5374
|
await rename(temp, pathOf(hash));
|
|
5250
5375
|
sizes.set(hash, size);
|
|
5251
5376
|
total += size;
|
|
5252
5377
|
return hash;
|
|
5253
5378
|
},
|
|
5254
|
-
|
|
5255
|
-
|
|
5379
|
+
has: (hash) => sizes.has(hash),
|
|
5380
|
+
read: async (hash) => readFile2(pathOf(hash)),
|
|
5381
|
+
writeTo: async (hash, absPath) => cloneOrCopy(pathOf(hash), absPath),
|
|
5256
5382
|
bytes: () => total,
|
|
5257
5383
|
size: (hash) => sizes.get(hash) ?? 0,
|
|
5258
5384
|
async prune(keep) {
|
|
@@ -5268,13 +5394,13 @@ async function openBlobStore(dir) {
|
|
|
5268
5394
|
|
|
5269
5395
|
// src/history/historyId.ts
|
|
5270
5396
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
5271
|
-
import { existsSync as existsSync11, mkdirSync as
|
|
5272
|
-
import { join as
|
|
5273
|
-
var ID_PATH =
|
|
5397
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync15, writeFileSync as writeFileSync7 } from "fs";
|
|
5398
|
+
import { join as join17, resolve as resolve6 } from "path";
|
|
5399
|
+
var ID_PATH = join17(".hyperframes", "history-id");
|
|
5274
5400
|
var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
5275
5401
|
function readId(projectDir) {
|
|
5276
5402
|
try {
|
|
5277
|
-
const id = readFileSync15(
|
|
5403
|
+
const id = readFileSync15(join17(projectDir, ID_PATH), "utf-8").trim();
|
|
5278
5404
|
return ID_SHAPE.test(id) ? id : null;
|
|
5279
5405
|
} catch {
|
|
5280
5406
|
return null;
|
|
@@ -5282,7 +5408,7 @@ function readId(projectDir) {
|
|
|
5282
5408
|
}
|
|
5283
5409
|
function recordedDir(historyDir) {
|
|
5284
5410
|
try {
|
|
5285
|
-
return JSON.parse(readFileSync15(
|
|
5411
|
+
return JSON.parse(readFileSync15(join17(historyDir, "project.json"), "utf-8")).dir ?? null;
|
|
5286
5412
|
} catch {
|
|
5287
5413
|
return null;
|
|
5288
5414
|
}
|
|
@@ -5290,21 +5416,21 @@ function recordedDir(historyDir) {
|
|
|
5290
5416
|
function projectHistoryId(projectDir, historyRoot) {
|
|
5291
5417
|
const dir = resolve6(projectDir);
|
|
5292
5418
|
let id = readId(dir);
|
|
5293
|
-
const was = id && recordedDir(
|
|
5419
|
+
const was = id && recordedDir(join17(historyRoot, id));
|
|
5294
5420
|
if (!id || was && was !== dir && existsSync11(was) && readId(was) === id) {
|
|
5295
5421
|
id = randomUUID6();
|
|
5296
|
-
|
|
5297
|
-
|
|
5422
|
+
mkdirSync8(join17(dir, ".hyperframes"), { recursive: true });
|
|
5423
|
+
writeFileSync7(join17(dir, ID_PATH), `${id}
|
|
5298
5424
|
`);
|
|
5299
5425
|
}
|
|
5300
|
-
|
|
5301
|
-
|
|
5426
|
+
mkdirSync8(join17(historyRoot, id), { recursive: true });
|
|
5427
|
+
writeFileSync7(join17(historyRoot, id, "project.json"), JSON.stringify({ dir }));
|
|
5302
5428
|
return id;
|
|
5303
5429
|
}
|
|
5304
5430
|
|
|
5305
5431
|
// src/history/historyLog.ts
|
|
5306
|
-
import { closeSync as closeSync5, constants as constants4, mkdirSync as
|
|
5307
|
-
import { dirname as
|
|
5432
|
+
import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync9, openSync as openSync5, readFileSync as readFileSync16, writeSync as writeSync2 } from "fs";
|
|
5433
|
+
import { dirname as dirname7 } from "path";
|
|
5308
5434
|
var START = "start";
|
|
5309
5435
|
function parseRecord(line) {
|
|
5310
5436
|
try {
|
|
@@ -5356,7 +5482,7 @@ function writeLog(file, log) {
|
|
|
5356
5482
|
...log.entries.map((entry) => ({ type: "entry", entry })),
|
|
5357
5483
|
...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
|
|
5358
5484
|
];
|
|
5359
|
-
|
|
5485
|
+
mkdirSync9(dirname7(file), { recursive: true });
|
|
5360
5486
|
replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
|
|
5361
5487
|
`).join(""), 420);
|
|
5362
5488
|
}
|
|
@@ -5414,17 +5540,33 @@ function referencedHashes(log, current) {
|
|
|
5414
5540
|
}
|
|
5415
5541
|
|
|
5416
5542
|
// src/history/projectHistory.ts
|
|
5543
|
+
var DEFAULT_HISTORY_ROOT = join18(homedir2(), ".cache", "hyperframes", "history");
|
|
5417
5544
|
var OUTSIDE = { kind: "outside", name: "Outside" };
|
|
5418
5545
|
var OUTSIDE_LABEL = "Changed outside the app";
|
|
5419
5546
|
var RACY_MS = 2e3;
|
|
5420
5547
|
var statKey = (file, sweptAt) => sweptAt - Math.max(file.mtimeMs, file.ctimeMs) < RACY_MS ? "" : `${file.size}:${file.mtimeMs}:${file.ctimeMs}`;
|
|
5548
|
+
var sameWho = (a, b) => a.kind === b.kind && a.name === b.name;
|
|
5549
|
+
function splitAt(change, at, kept) {
|
|
5550
|
+
if (at === change.after) return [change, null];
|
|
5551
|
+
if (!at || at === change.before || !kept) return [null, change];
|
|
5552
|
+
return [
|
|
5553
|
+
{ ...change, after: at },
|
|
5554
|
+
{ ...change, before: at }
|
|
5555
|
+
];
|
|
5556
|
+
}
|
|
5557
|
+
function addChange(group, path, before, after) {
|
|
5558
|
+
const earlier = group.changes.get(path);
|
|
5559
|
+
const from = earlier ? earlier.before : before;
|
|
5560
|
+
if (from === after) group.changes.delete(path);
|
|
5561
|
+
else group.changes.set(path, { path, before: from, after });
|
|
5562
|
+
}
|
|
5421
5563
|
var Engine = class {
|
|
5422
5564
|
constructor(options, projectId, blobs) {
|
|
5423
5565
|
this.options = options;
|
|
5424
5566
|
this.projectId = projectId;
|
|
5425
5567
|
this.blobs = blobs;
|
|
5426
5568
|
this.dir = resolve7(options.projectDir);
|
|
5427
|
-
this.home =
|
|
5569
|
+
this.home = join18(options.historyRoot, projectId);
|
|
5428
5570
|
}
|
|
5429
5571
|
options;
|
|
5430
5572
|
projectId;
|
|
@@ -5435,13 +5577,16 @@ var Engine = class {
|
|
|
5435
5577
|
tracked = /* @__PURE__ */ new Map();
|
|
5436
5578
|
windows = [];
|
|
5437
5579
|
outside = null;
|
|
5580
|
+
/** A coalescing claim, open until another key, its idle timer, or an operation commits it. */
|
|
5581
|
+
claimed = null;
|
|
5582
|
+
writeToken;
|
|
5438
5583
|
quietTimer;
|
|
5439
5584
|
maxTimer;
|
|
5440
5585
|
notedTimer = null;
|
|
5441
5586
|
listeners = /* @__PURE__ */ new Set();
|
|
5442
5587
|
tail = Promise.resolve();
|
|
5443
5588
|
get logFile() {
|
|
5444
|
-
return
|
|
5589
|
+
return join18(this.home, "log.jsonl");
|
|
5445
5590
|
}
|
|
5446
5591
|
now() {
|
|
5447
5592
|
return (this.options.now ?? Date.now)();
|
|
@@ -5474,7 +5619,7 @@ var Engine = class {
|
|
|
5474
5619
|
const sweptAt = Date.now();
|
|
5475
5620
|
for (const file of listProjectFiles(this.dir))
|
|
5476
5621
|
this.tracked.set(file.path, {
|
|
5477
|
-
hash: await this.blobs.put(
|
|
5622
|
+
hash: await this.blobs.put(join18(this.dir, file.path)),
|
|
5478
5623
|
stat: statKey(file, sweptAt)
|
|
5479
5624
|
});
|
|
5480
5625
|
this.log.baseline = this.manifest();
|
|
@@ -5486,7 +5631,7 @@ var Engine = class {
|
|
|
5486
5631
|
}
|
|
5487
5632
|
readStatCache() {
|
|
5488
5633
|
try {
|
|
5489
|
-
const files = JSON.parse(readFileSync17(
|
|
5634
|
+
const files = JSON.parse(readFileSync17(join18(this.home, "stat.json"), "utf-8"));
|
|
5490
5635
|
return new Map(Object.entries(files));
|
|
5491
5636
|
} catch {
|
|
5492
5637
|
return /* @__PURE__ */ new Map();
|
|
@@ -5494,8 +5639,8 @@ var Engine = class {
|
|
|
5494
5639
|
}
|
|
5495
5640
|
saveStatCache() {
|
|
5496
5641
|
const files = JSON.stringify(Object.fromEntries(this.tracked));
|
|
5497
|
-
|
|
5498
|
-
replaceFileAtomically(
|
|
5642
|
+
mkdirSync10(this.home, { recursive: true });
|
|
5643
|
+
replaceFileAtomically(join18(this.home, "stat.json"), files, 420);
|
|
5499
5644
|
}
|
|
5500
5645
|
/**
|
|
5501
5646
|
* Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
|
|
@@ -5520,7 +5665,7 @@ var Engine = class {
|
|
|
5520
5665
|
/** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
|
|
5521
5666
|
async storeIfPresent(path) {
|
|
5522
5667
|
try {
|
|
5523
|
-
return await this.blobs.put(
|
|
5668
|
+
return await this.blobs.put(join18(this.dir, path));
|
|
5524
5669
|
} catch (error) {
|
|
5525
5670
|
if (error.code === "ENOENT") return null;
|
|
5526
5671
|
throw error;
|
|
@@ -5538,11 +5683,96 @@ var Engine = class {
|
|
|
5538
5683
|
record(path, before, after) {
|
|
5539
5684
|
const window = this.windows.at(-1);
|
|
5540
5685
|
if (window) this.touch(window);
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5686
|
+
addChange(window ?? this.outsideGroup(), path, before, after);
|
|
5687
|
+
}
|
|
5688
|
+
/** ponytail: another's write between Studio's write and its sweep folds into Studio's entry (per-write tokens). */
|
|
5689
|
+
async claimNow(who, label, paths, { coalesceKey, idleMs, overwrote = {} }) {
|
|
5690
|
+
await this.sweep();
|
|
5691
|
+
const { taken, cut } = this.takeClaimed(who, paths, overwrote);
|
|
5692
|
+
if (!taken.length) {
|
|
5693
|
+
if (coalesceKey !== this.claimed?.key) await this.commitClaim();
|
|
5694
|
+
return null;
|
|
5695
|
+
}
|
|
5696
|
+
const held = this.heldFor(coalesceKey, taken);
|
|
5697
|
+
if (!held) await this.commitClaim();
|
|
5698
|
+
await this.commitOlder(cut);
|
|
5699
|
+
const group = held ?? this.newGroup(who, label);
|
|
5700
|
+
for (const change of taken) addChange(group, change.path, change.before, change.after);
|
|
5701
|
+
if (coalesceKey) return this.holdClaim(group, coalesceKey, idleMs);
|
|
5702
|
+
const entry = await this.commit(group);
|
|
5703
|
+
return entry && { id: entry.id };
|
|
5704
|
+
}
|
|
5705
|
+
async commitOlder(cut) {
|
|
5706
|
+
if (this.outside) cut.delete(this.outside);
|
|
5707
|
+
await this.commitOutside();
|
|
5708
|
+
for (const [window, paths] of cut) await this.commitCut(window, paths);
|
|
5709
|
+
}
|
|
5710
|
+
/** The held claim `taken` continues: same key, each file from its own last change, no held file changed since. */
|
|
5711
|
+
heldFor(key, taken) {
|
|
5712
|
+
const group = key && this.claimed?.key === key ? this.claimed.group : null;
|
|
5713
|
+
const after = (path, before) => {
|
|
5714
|
+
const own = group?.changes.get(path);
|
|
5715
|
+
return own ? own.after : before;
|
|
5716
|
+
};
|
|
5717
|
+
const takenPaths = new Set(taken.map((change) => change.path));
|
|
5718
|
+
const untouched = (change) => takenPaths.has(change.path) || (this.tracked.get(change.path)?.hash ?? null) === change.after;
|
|
5719
|
+
return group && taken.every((change) => after(change.path, change.before) === change.before) && [...group.changes.values()].every(untouched) ? group : null;
|
|
5720
|
+
}
|
|
5721
|
+
logPath(path) {
|
|
5722
|
+
return relative4(this.dir, resolve7(this.dir, path)).split(sep3).join("/");
|
|
5723
|
+
}
|
|
5724
|
+
/** Takes `paths`' uncommitted changes from others, cut at what `who` overwrote; `cut`: paths each group keeps. */
|
|
5725
|
+
takeClaimed(who, paths, overwrote) {
|
|
5726
|
+
const wanted = new Set(paths.map((path) => this.logPath(path)));
|
|
5727
|
+
const at = new Map(
|
|
5728
|
+
Object.entries(overwrote).map(([path, version]) => [
|
|
5729
|
+
this.logPath(path),
|
|
5730
|
+
hashOfVersion(version)
|
|
5731
|
+
])
|
|
5732
|
+
);
|
|
5733
|
+
const others = this.windows.filter((open) => !sameWho(open.who, who));
|
|
5734
|
+
const groups = this.outside ? [this.outside, ...others] : others;
|
|
5735
|
+
const taken = [];
|
|
5736
|
+
const cut = /* @__PURE__ */ new Map();
|
|
5737
|
+
for (const group of groups) {
|
|
5738
|
+
for (const change of [...group.changes.values()]) {
|
|
5739
|
+
if (!wanted.has(change.path)) continue;
|
|
5740
|
+
const [kept, claimed] = this.cutOut(group, change, at.get(change.path));
|
|
5741
|
+
if (claimed) taken.push(claimed);
|
|
5742
|
+
if (kept && claimed) cut.set(group, [...cut.get(group) ?? [], change.path]);
|
|
5743
|
+
}
|
|
5744
|
+
}
|
|
5745
|
+
return { taken, cut };
|
|
5746
|
+
}
|
|
5747
|
+
cutOut(group, change, cut) {
|
|
5748
|
+
const [kept, claimed] = splitAt(change, cut, cut !== void 0 && this.blobs.has(cut));
|
|
5749
|
+
group.changes.delete(change.path);
|
|
5750
|
+
if (kept) group.changes.set(kept.path, kept);
|
|
5751
|
+
return [kept, claimed];
|
|
5752
|
+
}
|
|
5753
|
+
/** Commits the window's cut `paths` as their own entry, other files staying: a window holds one change per file,
|
|
5754
|
+
* so the writer's next write to a cut file would otherwise span the claimer's edit. */
|
|
5755
|
+
async commitCut(window, paths) {
|
|
5756
|
+
const part = { ...this.newGroup(window.who, window.label), startedAt: window.startedAt };
|
|
5757
|
+
for (const path of paths) {
|
|
5758
|
+
part.changes.set(path, window.changes.get(path));
|
|
5759
|
+
window.changes.delete(path);
|
|
5760
|
+
}
|
|
5761
|
+
window.entry = await this.commit(part);
|
|
5762
|
+
}
|
|
5763
|
+
holdClaim(group, key, idleMs = this.options.quietMs ?? 2e3) {
|
|
5764
|
+
clearTimeout(this.claimed?.timer);
|
|
5765
|
+
const timer = setTimeout(() => this.background(() => this.commitClaim()), idleMs);
|
|
5766
|
+
timer.unref?.();
|
|
5767
|
+
this.claimed = { group, key, timer };
|
|
5768
|
+
return group.changes.size ? { id: group.id } : null;
|
|
5769
|
+
}
|
|
5770
|
+
async commitClaim() {
|
|
5771
|
+
const held = this.claimed;
|
|
5772
|
+
this.claimed = null;
|
|
5773
|
+
if (!held) return;
|
|
5774
|
+
clearTimeout(held.timer);
|
|
5775
|
+
await this.commit(held.group);
|
|
5546
5776
|
}
|
|
5547
5777
|
outsideGroup() {
|
|
5548
5778
|
const { quietMs = 2e3, maxGroupMs = 3e4 } = this.options;
|
|
@@ -5571,9 +5801,9 @@ var Engine = class {
|
|
|
5571
5801
|
}
|
|
5572
5802
|
async commit(group, extra = {}) {
|
|
5573
5803
|
if (!group.changes.size) return null;
|
|
5574
|
-
const {
|
|
5804
|
+
const { id, who, label, startedAt, changes } = group;
|
|
5575
5805
|
const files = [...changes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
5576
|
-
const entry = {
|
|
5806
|
+
const entry = { id, who, label, startedAt, endedAt: this.now(), files, ...extra };
|
|
5577
5807
|
this.log.entries.push(entry);
|
|
5578
5808
|
try {
|
|
5579
5809
|
saveRecord(this.logFile, this.log, { type: "entry", entry });
|
|
@@ -5604,6 +5834,7 @@ var Engine = class {
|
|
|
5604
5834
|
/** Before an operation: every write so far is filed, and the outside group is closed so it sorts first. */
|
|
5605
5835
|
async settle() {
|
|
5606
5836
|
await this.sweep();
|
|
5837
|
+
await this.commitClaim();
|
|
5607
5838
|
await this.commitOutside();
|
|
5608
5839
|
}
|
|
5609
5840
|
/** A watcher saw a write: one sweep per burst takes it in (a deleted folder is reported by its name alone). */
|
|
@@ -5646,12 +5877,13 @@ var Engine = class {
|
|
|
5646
5877
|
if (!this.windows.includes(window)) return window.entry ?? null;
|
|
5647
5878
|
clearTimeout(window.idleTimer);
|
|
5648
5879
|
this.windows = this.windows.filter((open) => open !== window);
|
|
5649
|
-
window.entry = await this.commit(window);
|
|
5880
|
+
window.entry = await this.commit(window) ?? window.entry ?? null;
|
|
5650
5881
|
return window.entry;
|
|
5651
5882
|
}
|
|
5652
5883
|
/** Every pending write, open window and outside group, committed: for flush and close. */
|
|
5653
5884
|
async settleAll() {
|
|
5654
5885
|
await this.sweep();
|
|
5886
|
+
await this.commitClaim();
|
|
5655
5887
|
for (const window of [...this.windows]) await this.endWindow(window);
|
|
5656
5888
|
await this.commitOutside();
|
|
5657
5889
|
}
|
|
@@ -5661,9 +5893,8 @@ var Engine = class {
|
|
|
5661
5893
|
this.windows.push(group);
|
|
5662
5894
|
try {
|
|
5663
5895
|
for (const [path, hash] of target) {
|
|
5664
|
-
if ((this.tracked.get(path)?.hash ?? null)
|
|
5665
|
-
|
|
5666
|
-
else await this.blobs.writeTo(hash, join17(this.dir, path));
|
|
5896
|
+
if ((this.tracked.get(path)?.hash ?? null) !== hash)
|
|
5897
|
+
await this.writeProjectFile(path, hash);
|
|
5667
5898
|
}
|
|
5668
5899
|
await this.sweep();
|
|
5669
5900
|
} finally {
|
|
@@ -5671,6 +5902,17 @@ var Engine = class {
|
|
|
5671
5902
|
}
|
|
5672
5903
|
return this.commit(group, extra);
|
|
5673
5904
|
}
|
|
5905
|
+
/** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
|
|
5906
|
+
async writeProjectFile(path, hash) {
|
|
5907
|
+
const absPath = join18(this.dir, path);
|
|
5908
|
+
const { writeToken } = this;
|
|
5909
|
+
if (writeToken) {
|
|
5910
|
+
const version = hash === null ? DELETED_VERSION : hashVersion(hash);
|
|
5911
|
+
recordFileWriteReceipt(absPath, { path, version, writeToken });
|
|
5912
|
+
}
|
|
5913
|
+
if (hash === null) await rm2(absPath, { force: true });
|
|
5914
|
+
else await this.blobs.writeTo(hash, absPath);
|
|
5915
|
+
}
|
|
5674
5916
|
entry(id) {
|
|
5675
5917
|
const entry = this.log.entries.find((candidate) => candidate.id === id);
|
|
5676
5918
|
if (!entry) throw new Error("That change is no longer kept in this project's history.");
|
|
@@ -5683,9 +5925,7 @@ var Engine = class {
|
|
|
5683
5925
|
}
|
|
5684
5926
|
async undoNow(id, who, mode) {
|
|
5685
5927
|
const entry = this.entry(id);
|
|
5686
|
-
const changed =
|
|
5687
|
-
(file) => (this.tracked.get(file.path)?.hash ?? null) !== file.after
|
|
5688
|
-
);
|
|
5928
|
+
const changed = this.movedOn(entry);
|
|
5689
5929
|
if (changed.length && !mode) return { ok: false, conflict: this.conflict(entry, changed) };
|
|
5690
5930
|
if (mode === "back-to-before") {
|
|
5691
5931
|
const index = this.log.entries.indexOf(entry);
|
|
@@ -5701,6 +5941,19 @@ var Engine = class {
|
|
|
5701
5941
|
entry: await this.writeAs(who, this.undoLabel(entry), target, { undoes: id })
|
|
5702
5942
|
};
|
|
5703
5943
|
}
|
|
5944
|
+
movedOn(entry) {
|
|
5945
|
+
return entry.files.filter((file) => (this.tracked.get(file.path)?.hash ?? null) !== file.after);
|
|
5946
|
+
}
|
|
5947
|
+
/** Back reverts the newest change in effect, unless its files moved on under an earlier-ending edit (a held drag
|
|
5948
|
+
* committed after an agent's turn): then that edit, if it still applies. */
|
|
5949
|
+
next(direction) {
|
|
5950
|
+
const top = stepTarget(this.log.entries, direction);
|
|
5951
|
+
const moved = new Set(top && direction === "back" ? this.movedOn(top).map((f) => f.path) : []);
|
|
5952
|
+
if (!top || !moved.size) return top;
|
|
5953
|
+
const undone = undoneIds(this.log.entries);
|
|
5954
|
+
const under = this.log.entries.slice(0, this.log.entries.indexOf(top)).reverse().find((e) => !e.undoes && !undone.has(e.id) && e.files.some((f) => moved.has(f.path)));
|
|
5955
|
+
return under && !this.movedOn(under).length ? under : top;
|
|
5956
|
+
}
|
|
5704
5957
|
conflict(entry, changed) {
|
|
5705
5958
|
const paths = new Set(changed.map((file) => file.path));
|
|
5706
5959
|
const after = this.log.entries.slice(this.log.entries.indexOf(entry) + 1);
|
|
@@ -5716,6 +5969,18 @@ var Engine = class {
|
|
|
5716
5969
|
for (const [path, hash] of files) target.set(path, hash);
|
|
5717
5970
|
return this.writeAs(who, label, target, { restoredTo: point });
|
|
5718
5971
|
}
|
|
5972
|
+
/** Settles, then runs `task` with its writes labelled `writeToken`. */
|
|
5973
|
+
operation(writeToken, task) {
|
|
5974
|
+
return this.queue(async () => {
|
|
5975
|
+
await this.settle();
|
|
5976
|
+
this.writeToken = writeToken;
|
|
5977
|
+
try {
|
|
5978
|
+
return await task();
|
|
5979
|
+
} finally {
|
|
5980
|
+
this.writeToken = void 0;
|
|
5981
|
+
}
|
|
5982
|
+
});
|
|
5983
|
+
}
|
|
5719
5984
|
pointLabel(point) {
|
|
5720
5985
|
return point === START ? "the start" : this.entry(point).label;
|
|
5721
5986
|
}
|
|
@@ -5723,6 +5988,7 @@ var Engine = class {
|
|
|
5723
5988
|
return {
|
|
5724
5989
|
projectId: this.projectId,
|
|
5725
5990
|
beginWindow: (who, label, options = {}) => this.beginWindow(who, label, options.idleMs ?? this.options.maxGroupMs ?? 3e4),
|
|
5991
|
+
claim: (who, label, paths, options = {}) => this.queue(() => this.claimNow(who, label, paths, options)),
|
|
5726
5992
|
noteChange: (path) => this.noteChange(path),
|
|
5727
5993
|
list: () => {
|
|
5728
5994
|
const undone = undoneIds(this.log.entries);
|
|
@@ -5732,23 +5998,20 @@ var Engine = class {
|
|
|
5732
5998
|
undone: undone.has(entry.id)
|
|
5733
5999
|
}));
|
|
5734
6000
|
},
|
|
5735
|
-
step: (direction, who) => this.
|
|
5736
|
-
|
|
5737
|
-
const target = stepTarget(this.log.entries, direction);
|
|
6001
|
+
step: (direction, who, { writeToken } = {}) => this.operation(writeToken, async () => {
|
|
6002
|
+
const target = this.next(direction);
|
|
5738
6003
|
return target ? this.undoNow(target.id, who) : { ok: true, entry: null };
|
|
5739
6004
|
}),
|
|
5740
|
-
undo: (id, { who, mode }) => this.
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
await this.settle();
|
|
5746
|
-
return this.restoreNow(point, who, `Restored: ${this.pointLabel(point)}`);
|
|
5747
|
-
}),
|
|
6005
|
+
undo: (id, { who, mode, writeToken }) => this.operation(writeToken, () => this.undoNow(id, who, mode)),
|
|
6006
|
+
restore: (point, who, { writeToken } = {}) => this.operation(
|
|
6007
|
+
writeToken,
|
|
6008
|
+
() => this.restoreNow(point, who, `Restored: ${this.pointLabel(point)}`)
|
|
6009
|
+
),
|
|
5748
6010
|
peek: (point) => {
|
|
5749
6011
|
const files = manifestAt(this.log, point);
|
|
5750
6012
|
return files && Object.fromEntries(files);
|
|
5751
6013
|
},
|
|
6014
|
+
next: (direction) => this.next(direction),
|
|
5752
6015
|
readBlob: (hash) => this.blobs.read(hash),
|
|
5753
6016
|
pin: (id, pinned) => {
|
|
5754
6017
|
this.entry(id);
|
|
@@ -5770,14 +6033,14 @@ var Engine = class {
|
|
|
5770
6033
|
};
|
|
5771
6034
|
async function openProjectHistory(options) {
|
|
5772
6035
|
const projectId = projectHistoryId(options.projectDir, options.historyRoot);
|
|
5773
|
-
const blobs = await openBlobStore(
|
|
6036
|
+
const blobs = await openBlobStore(join18(options.historyRoot, projectId, "blobs"));
|
|
5774
6037
|
const engine = new Engine(options, projectId, blobs);
|
|
5775
6038
|
await engine.queue(() => engine.start());
|
|
5776
6039
|
return engine.api();
|
|
5777
6040
|
}
|
|
5778
6041
|
|
|
5779
6042
|
// src/helpers/duplicateElement.ts
|
|
5780
|
-
import { ensureHfIds as
|
|
6043
|
+
import { ensureHfIds as ensureHfIds4 } from "@hyperframes/parsers/hf-ids";
|
|
5781
6044
|
function duplicateElementInHtml(source, target, newId, at) {
|
|
5782
6045
|
const { document, wrappedFragment } = parseSourceDocument(source);
|
|
5783
6046
|
const element = findTargetElement(document, target);
|
|
@@ -5797,7 +6060,7 @@ function duplicateElementInHtml(source, target, newId, at) {
|
|
|
5797
6060
|
clone.setAttribute("data-start", String(at));
|
|
5798
6061
|
element.parentElement.insertBefore(clone, element.nextSibling);
|
|
5799
6062
|
const html = wrappedFragment ? document.body.innerHTML || "" : document.toString();
|
|
5800
|
-
return { html:
|
|
6063
|
+
return { html: ensureHfIds4(html), matched: true, newId: uniqueId };
|
|
5801
6064
|
}
|
|
5802
6065
|
function nextUniqueId(document, newId) {
|
|
5803
6066
|
let uniqueId = newId;
|
|
@@ -5827,6 +6090,35 @@ function numericAttribute(element, name) {
|
|
|
5827
6090
|
return Number.isFinite(value) ? value : null;
|
|
5828
6091
|
}
|
|
5829
6092
|
|
|
6093
|
+
// src/helpers/previewDocumentStore.ts
|
|
6094
|
+
import { createHash as createHash7 } from "crypto";
|
|
6095
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync18, readdirSync as readdirSync7, rmSync as rmSync5 } from "fs";
|
|
6096
|
+
import { join as join19 } from "path";
|
|
6097
|
+
function createPreviewDocumentStore(dir, salt) {
|
|
6098
|
+
const fileFor = (key) => join19(dir, `${createHash7("sha256").update(`${salt}
|
|
6099
|
+
${key}`).digest("hex").slice(0, 32)}.html`);
|
|
6100
|
+
return {
|
|
6101
|
+
read(key) {
|
|
6102
|
+
try {
|
|
6103
|
+
return readFileSync18(fileFor(key), "utf-8");
|
|
6104
|
+
} catch {
|
|
6105
|
+
return null;
|
|
6106
|
+
}
|
|
6107
|
+
},
|
|
6108
|
+
write(key, html) {
|
|
6109
|
+
try {
|
|
6110
|
+
mkdirSync11(dir, { recursive: true });
|
|
6111
|
+
const file = fileFor(key);
|
|
6112
|
+
replaceFileAtomically(file, html, 420);
|
|
6113
|
+
for (const name of readdirSync7(dir)) {
|
|
6114
|
+
if (join19(dir, name) !== file) rmSync5(join19(dir, name), { force: true });
|
|
6115
|
+
}
|
|
6116
|
+
} catch {
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
};
|
|
6120
|
+
}
|
|
6121
|
+
|
|
5830
6122
|
// src/helpers/thumbnailOutput.ts
|
|
5831
6123
|
function thumbnailDeviceScaleFactor({
|
|
5832
6124
|
width,
|
|
@@ -5895,6 +6187,8 @@ function updateBackgroundRemovalProgress(state, event) {
|
|
|
5895
6187
|
state.avgMsPerFrame = event.avgMsPerFrame;
|
|
5896
6188
|
}
|
|
5897
6189
|
export {
|
|
6190
|
+
DEFAULT_HISTORY_ROOT,
|
|
6191
|
+
DELETED_VERSION,
|
|
5898
6192
|
START as HISTORY_START,
|
|
5899
6193
|
MIME_TYPES,
|
|
5900
6194
|
PREVIEW_BUNDLE_OPTIONS,
|
|
@@ -5906,6 +6200,7 @@ export {
|
|
|
5906
6200
|
compositionsAffectedBy,
|
|
5907
6201
|
consumeFileWriteReceipt,
|
|
5908
6202
|
createBackgroundRemovalJob,
|
|
6203
|
+
createPreviewDocumentStore,
|
|
5909
6204
|
createProjectSignature,
|
|
5910
6205
|
createStudioApi,
|
|
5911
6206
|
createStudioManualEditsRenderBodyScript,
|
|
@@ -5926,6 +6221,7 @@ export {
|
|
|
5926
6221
|
requestSubPath,
|
|
5927
6222
|
settledFileTag,
|
|
5928
6223
|
splitElementInHtml,
|
|
6224
|
+
stampProjectHfIds,
|
|
5929
6225
|
thumbnailDeviceScaleFactor,
|
|
5930
6226
|
walkDir
|
|
5931
6227
|
};
|