@hyperframes/studio-server 0.8.75 → 0.8.77
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-KFPWE4Q7.js → chunk-QYDRVMJ6.js} +3 -2
- package/dist/chunk-QYDRVMJ6.js.map +1 -0
- package/dist/{chunk-PGL4S2ZU.js → chunk-X3T65KC2.js} +5 -8
- package/dist/chunk-X3T65KC2.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 +23 -4
- package/dist/index.js +778 -475
- package/dist/index.js.map +1 -1
- package/dist/{mediaProxyPreview-FxDAeURd.d.ts → mediaProxyPreview-CIayJ4Y4.d.ts} +35 -19
- 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-X3T65KC2.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,
|
|
@@ -1040,11 +1133,12 @@ function requestSubPath(url, route) {
|
|
|
1040
1133
|
}
|
|
1041
1134
|
|
|
1042
1135
|
// src/routes/files.ts
|
|
1136
|
+
import { insertBeforeCloseTag } from "@hyperframes/core/compiler/html-document";
|
|
1043
1137
|
async function loadGsapParser() {
|
|
1044
1138
|
return import("@hyperframes/parsers/gsap-parser-recast");
|
|
1045
1139
|
}
|
|
1046
1140
|
function isDanglingSymlinkInProject(projectDir, lexicalPath) {
|
|
1047
|
-
if (!isSafePath(projectDir,
|
|
1141
|
+
if (!isSafePath(projectDir, dirname4(lexicalPath))) return false;
|
|
1048
1142
|
let stats;
|
|
1049
1143
|
try {
|
|
1050
1144
|
stats = lstatSync3(lexicalPath);
|
|
@@ -1052,7 +1146,7 @@ function isDanglingSymlinkInProject(projectDir, lexicalPath) {
|
|
|
1052
1146
|
return false;
|
|
1053
1147
|
}
|
|
1054
1148
|
if (!stats.isSymbolicLink()) return false;
|
|
1055
|
-
const target = resolve3(
|
|
1149
|
+
const target = resolve3(dirname4(lexicalPath), readlinkSync(lexicalPath));
|
|
1056
1150
|
if (!isSafePath(projectDir, target)) return false;
|
|
1057
1151
|
try {
|
|
1058
1152
|
statSync3(lexicalPath);
|
|
@@ -1151,7 +1245,7 @@ function commitElementPatchBatches(projectDir, batches, writeFile = (path, conte
|
|
|
1151
1245
|
resolvedPaths.add(absPath);
|
|
1152
1246
|
let before;
|
|
1153
1247
|
try {
|
|
1154
|
-
before =
|
|
1248
|
+
before = readFileSync6(absPath, "utf-8");
|
|
1155
1249
|
} catch {
|
|
1156
1250
|
return { error: "not-found", sourceFile: batch.sourceFile };
|
|
1157
1251
|
}
|
|
@@ -1202,7 +1296,7 @@ function commitElementPatchBatchesWithReceipts(c, projectDir, batches) {
|
|
|
1202
1296
|
return commitElementPatchBatches(
|
|
1203
1297
|
projectDir,
|
|
1204
1298
|
batches,
|
|
1205
|
-
|
|
1299
|
+
writeFileSync5,
|
|
1206
1300
|
c.req.header("X-Hyperframes-Write-Token")
|
|
1207
1301
|
);
|
|
1208
1302
|
}
|
|
@@ -1260,8 +1354,8 @@ async function parseMutationBody(c) {
|
|
|
1260
1354
|
return { target: body.target, body };
|
|
1261
1355
|
}
|
|
1262
1356
|
function ensureDir(filePath) {
|
|
1263
|
-
const dir =
|
|
1264
|
-
if (!existsSync3(dir))
|
|
1357
|
+
const dir = dirname4(filePath);
|
|
1358
|
+
if (!existsSync3(dir)) mkdirSync4(dir, { recursive: true });
|
|
1265
1359
|
}
|
|
1266
1360
|
function generateCopyPath(projectDir, originalPath) {
|
|
1267
1361
|
const ext = originalPath.includes(".") ? "." + originalPath.split(".").pop() : "";
|
|
@@ -1279,7 +1373,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1279
1373
|
function walkFiles(dir, filter) {
|
|
1280
1374
|
const results = [];
|
|
1281
1375
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
1282
|
-
const full =
|
|
1376
|
+
const full = join7(dir, entry.name);
|
|
1283
1377
|
if (entry.isDirectory()) {
|
|
1284
1378
|
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders" || entry.name === ".transcode-cache")
|
|
1285
1379
|
continue;
|
|
@@ -1298,7 +1392,7 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
1298
1392
|
let updatedCount = 0;
|
|
1299
1393
|
for (const file of textFiles) {
|
|
1300
1394
|
if (!isSafePath(projectDir, file)) continue;
|
|
1301
|
-
const content =
|
|
1395
|
+
const content = readFileSync6(file, "utf-8");
|
|
1302
1396
|
if (!content.includes(oldPath)) continue;
|
|
1303
1397
|
const updated = content.split(oldPath).join(newPath);
|
|
1304
1398
|
if (updated !== content) {
|
|
@@ -1309,7 +1403,7 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
1309
1403
|
return updatedCount;
|
|
1310
1404
|
}
|
|
1311
1405
|
function extractGsapScriptBlock(html) {
|
|
1312
|
-
const { document } = parseHTML2(html);
|
|
1406
|
+
const { document } = parseHTML2(ensureHfIds2(html));
|
|
1313
1407
|
const scripts = [
|
|
1314
1408
|
...document.querySelectorAll("script:not([src])"),
|
|
1315
1409
|
...Array.from(document.querySelectorAll("template")).flatMap(
|
|
@@ -1572,7 +1666,7 @@ function containsRawGsapExpression(value) {
|
|
|
1572
1666
|
return Object.values(value).some(containsRawGsapExpression);
|
|
1573
1667
|
}
|
|
1574
1668
|
async function prepareGsapMutationScript(c, res, firstMutation) {
|
|
1575
|
-
const beforeHtml =
|
|
1669
|
+
const beforeHtml = readFileSync6(res.absPath, "utf-8");
|
|
1576
1670
|
let html = beforeHtml;
|
|
1577
1671
|
let block = extractGsapScriptBlock(html);
|
|
1578
1672
|
if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
|
|
@@ -1586,8 +1680,8 @@ async function prepareGsapMutationScript(c, res, firstMutation) {
|
|
|
1586
1680
|
`window.__timelines["${compId}"] = tl;`,
|
|
1587
1681
|
"</script>"
|
|
1588
1682
|
].join("\n");
|
|
1589
|
-
html = html
|
|
1590
|
-
|
|
1683
|
+
html = insertBeforeCloseTag(html, "body", `${bootstrap}
|
|
1684
|
+
`) ?? `${html}
|
|
1591
1685
|
${bootstrap}`;
|
|
1592
1686
|
block = extractGsapScriptBlock(html);
|
|
1593
1687
|
}
|
|
@@ -1639,7 +1733,7 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1639
1733
|
const changed = block.scriptText !== initialScript;
|
|
1640
1734
|
const newHtml = changed ? block.replaceScript(block.scriptText) : html;
|
|
1641
1735
|
let backupPath = null;
|
|
1642
|
-
if (
|
|
1736
|
+
if (readFileSync6(res.absPath, "utf-8") !== beforeHtml) {
|
|
1643
1737
|
return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
|
|
1644
1738
|
}
|
|
1645
1739
|
if (changed) {
|
|
@@ -2420,11 +2514,11 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2420
2514
|
let written = false;
|
|
2421
2515
|
while (n < MAX_COPY_INDEX && isSafePath(projectDir, finalPath)) {
|
|
2422
2516
|
try {
|
|
2423
|
-
const fd =
|
|
2517
|
+
const fd = openSync2(finalPath, "wx");
|
|
2424
2518
|
try {
|
|
2425
|
-
|
|
2519
|
+
writeFileSync5(fd, buffer);
|
|
2426
2520
|
} finally {
|
|
2427
|
-
|
|
2521
|
+
closeSync2(fd);
|
|
2428
2522
|
}
|
|
2429
2523
|
written = true;
|
|
2430
2524
|
break;
|
|
@@ -2441,7 +2535,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2441
2535
|
if (n >= MAX_COPY_INDEX) skipped.push(name);
|
|
2442
2536
|
continue;
|
|
2443
2537
|
}
|
|
2444
|
-
const relativePath = subDir ?
|
|
2538
|
+
const relativePath = subDir ? join7(subDir, finalName) : finalName;
|
|
2445
2539
|
uploaded.push(relativePath);
|
|
2446
2540
|
if (isAudioFile(finalName)) {
|
|
2447
2541
|
generateWaveformCache(projectDir, relativePath).catch(() => {
|
|
@@ -2456,7 +2550,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2456
2550
|
if ("error" in res) return res.error;
|
|
2457
2551
|
let fd;
|
|
2458
2552
|
try {
|
|
2459
|
-
fd =
|
|
2553
|
+
fd = openSync2(res.absPath, "r");
|
|
2460
2554
|
} catch {
|
|
2461
2555
|
if (c.req.query("optional") === "1") {
|
|
2462
2556
|
return c.json({ filename: res.filePath, content: "", missing: true });
|
|
@@ -2464,10 +2558,10 @@ function registerFileRoutes(api, adapter) {
|
|
|
2464
2558
|
return c.json({ error: "not found" }, 404);
|
|
2465
2559
|
}
|
|
2466
2560
|
try {
|
|
2467
|
-
if (!
|
|
2561
|
+
if (!fstatSync2(fd).isFile()) {
|
|
2468
2562
|
return c.json({ error: "not found", why: "not_a_file" }, 404);
|
|
2469
2563
|
}
|
|
2470
|
-
const content =
|
|
2564
|
+
const content = readFileSync6(fd);
|
|
2471
2565
|
const version = fileContentVersion(content);
|
|
2472
2566
|
c.header("ETag", version);
|
|
2473
2567
|
return c.json({
|
|
@@ -2477,7 +2571,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2477
2571
|
missing: false
|
|
2478
2572
|
});
|
|
2479
2573
|
} finally {
|
|
2480
|
-
|
|
2574
|
+
closeSync2(fd);
|
|
2481
2575
|
}
|
|
2482
2576
|
});
|
|
2483
2577
|
api.put("/projects/:id/files/*", async (c) => {
|
|
@@ -2489,7 +2583,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2489
2583
|
if (expectedVersion === null && !createOnly) {
|
|
2490
2584
|
let currentContent = null;
|
|
2491
2585
|
try {
|
|
2492
|
-
currentContent =
|
|
2586
|
+
currentContent = readFileSync6(res.absPath);
|
|
2493
2587
|
} catch (error) {
|
|
2494
2588
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2495
2589
|
throw error;
|
|
@@ -2510,12 +2604,12 @@ function registerFileRoutes(api, adapter) {
|
|
|
2510
2604
|
ensureDir(res.absPath);
|
|
2511
2605
|
let fd;
|
|
2512
2606
|
try {
|
|
2513
|
-
fd =
|
|
2607
|
+
fd = openSync2(res.absPath, "wx");
|
|
2514
2608
|
} catch (error) {
|
|
2515
2609
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
2516
2610
|
throw error;
|
|
2517
2611
|
}
|
|
2518
|
-
const currentContent =
|
|
2612
|
+
const currentContent = readFileSync6(res.absPath);
|
|
2519
2613
|
return c.json(
|
|
2520
2614
|
{
|
|
2521
2615
|
error: "file conflict",
|
|
@@ -2529,12 +2623,12 @@ function registerFileRoutes(api, adapter) {
|
|
|
2529
2623
|
try {
|
|
2530
2624
|
writeSync(fd, body, 0, body.length, 0);
|
|
2531
2625
|
} finally {
|
|
2532
|
-
|
|
2626
|
+
closeSync2(fd);
|
|
2533
2627
|
}
|
|
2534
2628
|
} else {
|
|
2535
2629
|
let fd;
|
|
2536
2630
|
try {
|
|
2537
|
-
fd =
|
|
2631
|
+
fd = openSync2(res.absPath, "r+");
|
|
2538
2632
|
} catch (error) {
|
|
2539
2633
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2540
2634
|
throw error;
|
|
@@ -2550,7 +2644,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2550
2644
|
);
|
|
2551
2645
|
}
|
|
2552
2646
|
try {
|
|
2553
|
-
const currentContent =
|
|
2647
|
+
const currentContent = readFileSync6(fd);
|
|
2554
2648
|
const currentVersion = fileContentVersion(currentContent);
|
|
2555
2649
|
if (expectedVersion !== currentVersion) {
|
|
2556
2650
|
return c.json(
|
|
@@ -2565,12 +2659,12 @@ function registerFileRoutes(api, adapter) {
|
|
|
2565
2659
|
}
|
|
2566
2660
|
backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
2567
2661
|
if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
|
|
2568
|
-
const mode =
|
|
2569
|
-
|
|
2662
|
+
const mode = fstatSync2(fd).mode;
|
|
2663
|
+
closeSync2(fd);
|
|
2570
2664
|
fd = null;
|
|
2571
2665
|
replaceFileAtomically(res.absPath, body, mode);
|
|
2572
2666
|
} finally {
|
|
2573
|
-
if (fd !== null)
|
|
2667
|
+
if (fd !== null) closeSync2(fd);
|
|
2574
2668
|
}
|
|
2575
2669
|
}
|
|
2576
2670
|
const version = fileContentVersion(body);
|
|
@@ -2591,7 +2685,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2591
2685
|
ensureDir(res.absPath);
|
|
2592
2686
|
const body = Buffer.from(await c.req.arrayBuffer());
|
|
2593
2687
|
try {
|
|
2594
|
-
|
|
2688
|
+
writeFileSync5(res.absPath, body, { flag: "wx" });
|
|
2595
2689
|
} catch (error) {
|
|
2596
2690
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
2597
2691
|
throw error;
|
|
@@ -2625,7 +2719,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2625
2719
|
}
|
|
2626
2720
|
let before;
|
|
2627
2721
|
try {
|
|
2628
|
-
before =
|
|
2722
|
+
before = readFileSync6(ctx.absPath, "utf-8");
|
|
2629
2723
|
} catch (error) {
|
|
2630
2724
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2631
2725
|
throw error;
|
|
@@ -2682,7 +2776,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2682
2776
|
}
|
|
2683
2777
|
const parsed = await parseMutationBody(c);
|
|
2684
2778
|
if ("error" in parsed) return parsed.error;
|
|
2685
|
-
const originalContent =
|
|
2779
|
+
const originalContent = readFileSync6(ctx.absPath, "utf-8");
|
|
2686
2780
|
return writeIfChanged(
|
|
2687
2781
|
c,
|
|
2688
2782
|
ctx.project.dir,
|
|
@@ -2703,7 +2797,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2703
2797
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
2704
2798
|
return c.json({ error: "targets required" }, 400);
|
|
2705
2799
|
}
|
|
2706
|
-
const originalContent =
|
|
2800
|
+
const originalContent = readFileSync6(ctx.absPath, "utf-8");
|
|
2707
2801
|
let next = originalContent;
|
|
2708
2802
|
for (const target of targets) {
|
|
2709
2803
|
next = removeElementFromHtml(next, target);
|
|
@@ -2736,7 +2830,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2736
2830
|
seen.add(absPath);
|
|
2737
2831
|
let before;
|
|
2738
2832
|
try {
|
|
2739
|
-
before =
|
|
2833
|
+
before = readFileSync6(absPath, "utf-8");
|
|
2740
2834
|
} catch {
|
|
2741
2835
|
return c.json({ error: `not found: ${file.path}` }, 404);
|
|
2742
2836
|
}
|
|
@@ -2763,7 +2857,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2763
2857
|
prepared.push(folded);
|
|
2764
2858
|
}
|
|
2765
2859
|
for (const file of prepared) {
|
|
2766
|
-
const current =
|
|
2860
|
+
const current = readFileSync6(file.absPath, "utf-8");
|
|
2767
2861
|
if (current !== file.before) {
|
|
2768
2862
|
return c.json(
|
|
2769
2863
|
{
|
|
@@ -2805,7 +2899,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2805
2899
|
const conflicts = [];
|
|
2806
2900
|
for (const file of written.reverse()) {
|
|
2807
2901
|
try {
|
|
2808
|
-
const current =
|
|
2902
|
+
const current = readFileSync6(file.absPath, "utf-8");
|
|
2809
2903
|
if (current !== file.after) {
|
|
2810
2904
|
conflicts.push(file.path);
|
|
2811
2905
|
continue;
|
|
@@ -2853,7 +2947,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2853
2947
|
const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
|
|
2854
2948
|
let originalContent;
|
|
2855
2949
|
try {
|
|
2856
|
-
originalContent =
|
|
2950
|
+
originalContent = readFileSync6(ctx.absPath, "utf-8");
|
|
2857
2951
|
} catch {
|
|
2858
2952
|
return c.json({ error: "not found" }, 404);
|
|
2859
2953
|
}
|
|
@@ -2909,7 +3003,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2909
3003
|
}
|
|
2910
3004
|
let originalContent;
|
|
2911
3005
|
try {
|
|
2912
|
-
originalContent =
|
|
3006
|
+
originalContent = readFileSync6(ctx.absPath, "utf-8");
|
|
2913
3007
|
} catch {
|
|
2914
3008
|
return c.json({ error: "not found" }, 404);
|
|
2915
3009
|
}
|
|
@@ -3010,7 +3104,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3010
3104
|
}
|
|
3011
3105
|
let originalContent;
|
|
3012
3106
|
try {
|
|
3013
|
-
originalContent =
|
|
3107
|
+
originalContent = readFileSync6(ctx.absPath, "utf-8");
|
|
3014
3108
|
} catch {
|
|
3015
3109
|
return c.json({ error: "not found" }, 404);
|
|
3016
3110
|
}
|
|
@@ -3065,7 +3159,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3065
3159
|
).map((entry) => ({ target: entry.target, track: entry.track }));
|
|
3066
3160
|
let originalContent;
|
|
3067
3161
|
try {
|
|
3068
|
-
originalContent =
|
|
3162
|
+
originalContent = readFileSync6(ctx.absPath, "utf-8");
|
|
3069
3163
|
} catch {
|
|
3070
3164
|
return c.json({ error: "not found" }, 404);
|
|
3071
3165
|
}
|
|
@@ -3094,7 +3188,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3094
3188
|
if ("error" in parsed) return parsed.error;
|
|
3095
3189
|
let content;
|
|
3096
3190
|
try {
|
|
3097
|
-
content =
|
|
3191
|
+
content = readFileSync6(ctx.absPath, "utf-8");
|
|
3098
3192
|
} catch {
|
|
3099
3193
|
return c.json({ exists: false });
|
|
3100
3194
|
}
|
|
@@ -3137,7 +3231,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3137
3231
|
return c.json({ error: "forbidden" }, 403);
|
|
3138
3232
|
}
|
|
3139
3233
|
ensureDir(destAbs);
|
|
3140
|
-
|
|
3234
|
+
writeFileSync5(destAbs, readFileSync6(srcAbs));
|
|
3141
3235
|
return c.json({ ok: true, path: copyPath }, 201);
|
|
3142
3236
|
});
|
|
3143
3237
|
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
@@ -3153,7 +3247,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3153
3247
|
const subDir = c.req.query("dir") ?? "";
|
|
3154
3248
|
const targetDir = subDir ? resolveWithinProject(project.dir, subDir) : project.dir;
|
|
3155
3249
|
if (!targetDir) return c.json({ error: "forbidden" }, 403);
|
|
3156
|
-
if (subDir && !existsSync3(targetDir))
|
|
3250
|
+
if (subDir && !existsSync3(targetDir)) mkdirSync4(targetDir, { recursive: true });
|
|
3157
3251
|
const formData = await c.req.formData();
|
|
3158
3252
|
const result = await processUploadedFiles(formData, targetDir, project.dir);
|
|
3159
3253
|
return c.json(
|
|
@@ -3167,7 +3261,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3167
3261
|
mustExist: true
|
|
3168
3262
|
});
|
|
3169
3263
|
if ("error" in res) return res.error;
|
|
3170
|
-
const html =
|
|
3264
|
+
const html = readFileSync6(res.absPath, "utf-8");
|
|
3171
3265
|
const block = extractGsapScriptBlock(html);
|
|
3172
3266
|
if (!block) {
|
|
3173
3267
|
return c.json({
|
|
@@ -3216,7 +3310,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3216
3310
|
if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
|
|
3217
3311
|
return c.json({ error: "expected and restore contents required" }, 400);
|
|
3218
3312
|
}
|
|
3219
|
-
const current =
|
|
3313
|
+
const current = readFileSync6(res.absPath, "utf-8");
|
|
3220
3314
|
if (current !== body.expected) {
|
|
3221
3315
|
return c.json({ ok: true, restored: false, conflict: true });
|
|
3222
3316
|
}
|
|
@@ -3226,19 +3320,20 @@ function registerFileRoutes(api, adapter) {
|
|
|
3226
3320
|
}
|
|
3227
3321
|
|
|
3228
3322
|
// src/routes/preview.ts
|
|
3229
|
-
import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync8, statSync as
|
|
3323
|
+
import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
|
|
3230
3324
|
import { Readable } from "stream";
|
|
3231
|
-
import { join as
|
|
3232
|
-
import { createHash as
|
|
3325
|
+
import { join as join9, resolve as resolve4 } from "path";
|
|
3326
|
+
import { createHash as createHash4 } from "crypto";
|
|
3233
3327
|
import {
|
|
3234
3328
|
injectScriptsIntoHtml,
|
|
3329
|
+
insertBeforeCloseTag as insertBeforeCloseTag2,
|
|
3235
3330
|
stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2
|
|
3236
3331
|
} from "@hyperframes/core/compiler";
|
|
3237
3332
|
import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
3238
3333
|
|
|
3239
3334
|
// src/helpers/subComposition.ts
|
|
3240
|
-
import { existsSync as existsSync4, readFileSync as
|
|
3241
|
-
import { join as
|
|
3335
|
+
import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
|
|
3336
|
+
import { join as join8 } from "path";
|
|
3242
3337
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
3243
3338
|
import {
|
|
3244
3339
|
rewriteAssetPaths,
|
|
@@ -3250,7 +3345,7 @@ function isFullHtmlDocument(html) {
|
|
|
3250
3345
|
return /^\s*(?:<!doctype\s|<html[\s>])/i.test(html);
|
|
3251
3346
|
}
|
|
3252
3347
|
function rewriteRelativePaths(root, compPath, projectDir) {
|
|
3253
|
-
const assetExists = (path) => existsSync4(
|
|
3348
|
+
const assetExists = (path) => existsSync4(join8(projectDir, path));
|
|
3254
3349
|
rewriteAssetPaths(
|
|
3255
3350
|
root.querySelectorAll("[src], [href]"),
|
|
3256
3351
|
compPath,
|
|
@@ -3347,9 +3442,9 @@ function tagRootCompositionFile(bodyHtml, compPath) {
|
|
|
3347
3442
|
return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
|
|
3348
3443
|
}
|
|
3349
3444
|
function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
|
|
3350
|
-
const compFile =
|
|
3445
|
+
const compFile = join8(projectDir, compPath);
|
|
3351
3446
|
if (!existsSync4(compFile)) return null;
|
|
3352
|
-
const rawComp = rawOverride ??
|
|
3447
|
+
const rawComp = rawOverride ?? readFileSync7(compFile, "utf-8");
|
|
3353
3448
|
let compHeadContent = "";
|
|
3354
3449
|
let rewrittenContent;
|
|
3355
3450
|
let htmlAttrs = "";
|
|
@@ -3379,10 +3474,10 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
3379
3474
|
}
|
|
3380
3475
|
rewrittenContent = stripEmbeddedRuntimeScripts(rewrittenContent);
|
|
3381
3476
|
rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
|
|
3382
|
-
const indexPath =
|
|
3477
|
+
const indexPath = join8(projectDir, "index.html");
|
|
3383
3478
|
let headContent = "";
|
|
3384
3479
|
if (existsSync4(indexPath)) {
|
|
3385
|
-
const indexHtml =
|
|
3480
|
+
const indexHtml = readFileSync7(indexPath, "utf-8");
|
|
3386
3481
|
const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
3387
3482
|
headContent = headMatch?.[1] ?? "";
|
|
3388
3483
|
}
|
|
@@ -3419,63 +3514,7 @@ function hasBaseElement(html) {
|
|
|
3419
3514
|
}
|
|
3420
3515
|
|
|
3421
3516
|
// 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
|
-
}
|
|
3517
|
+
import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
|
|
3479
3518
|
|
|
3480
3519
|
// src/helpers/variablesPayload.ts
|
|
3481
3520
|
var VARIABLES_PAYLOAD_ERROR = "variables must be a JSON object of {variableId: value}";
|
|
@@ -3513,13 +3552,12 @@ function injectProjectSignature(html, signature) {
|
|
|
3513
3552
|
tag
|
|
3514
3553
|
);
|
|
3515
3554
|
}
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
return `${tag}
|
|
3555
|
+
return insertBeforeCloseTag2(html, "head", `${tag}
|
|
3556
|
+
`) ?? `${tag}
|
|
3519
3557
|
${html}`;
|
|
3520
3558
|
}
|
|
3521
3559
|
function readStudioMotionManifestContent(projectDir) {
|
|
3522
|
-
const manifestPath =
|
|
3560
|
+
const manifestPath = join9(projectDir, STUDIO_MOTION_PATH);
|
|
3523
3561
|
if (!existsSync5(manifestPath)) return "";
|
|
3524
3562
|
try {
|
|
3525
3563
|
return readFileSync8(manifestPath, "utf-8");
|
|
@@ -3540,9 +3578,8 @@ function parseStudioMotionManifestContent(content) {
|
|
|
3540
3578
|
}
|
|
3541
3579
|
}
|
|
3542
3580
|
function injectScriptTagIntoHead(html, scriptTag) {
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
return `${scriptTag}
|
|
3581
|
+
return insertBeforeCloseTag2(html, "head", `${scriptTag}
|
|
3582
|
+
`) ?? `${scriptTag}
|
|
3546
3583
|
${html}`;
|
|
3547
3584
|
}
|
|
3548
3585
|
function htmlHasGsap(html) {
|
|
@@ -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,35 +3729,28 @@ 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
|
}
|
|
3719
3750
|
if (!bundled.includes("hyperframe.runtime") && !bundled.includes("hyperframes-preview-runtime")) {
|
|
3720
3751
|
const runtimeTag = `<script src="${adapter.runtimeUrl}"></script>`;
|
|
3721
|
-
bundled = bundled
|
|
3722
|
-
|
|
3752
|
+
bundled = insertBeforeCloseTag2(bundled, "body", `${runtimeTag}
|
|
3753
|
+
`) ?? `${bundled}
|
|
3723
3754
|
${runtimeTag}`;
|
|
3724
3755
|
}
|
|
3725
3756
|
const baseHref = `/api/projects/${project.id}/preview/`;
|
|
@@ -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;
|
|
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
|
+
});
|
|
3770
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")) {
|
|
@@ -5106,117 +5174,6 @@ function registerGlobalAssetRoutes(api) {
|
|
|
5106
5174
|
api.get("/assets/global", (c) => c.json({ assets: readGlobalAssets().map(toPublicAsset) }));
|
|
5107
5175
|
}
|
|
5108
5176
|
|
|
5109
|
-
// src/history/historyLog.ts
|
|
5110
|
-
import { closeSync as closeSync5, constants as constants3, mkdirSync as mkdirSync7, openSync as openSync5, readFileSync as readFileSync15, writeSync as writeSync2 } from "fs";
|
|
5111
|
-
import { dirname as dirname5 } from "path";
|
|
5112
|
-
var START = "start";
|
|
5113
|
-
function parseRecord(line) {
|
|
5114
|
-
try {
|
|
5115
|
-
return JSON.parse(line);
|
|
5116
|
-
} catch {
|
|
5117
|
-
return null;
|
|
5118
|
-
}
|
|
5119
|
-
}
|
|
5120
|
-
function readLog(file, onUnreadable) {
|
|
5121
|
-
let text2;
|
|
5122
|
-
try {
|
|
5123
|
-
text2 = readFileSync15(file, "utf-8");
|
|
5124
|
-
} catch {
|
|
5125
|
-
return null;
|
|
5126
|
-
}
|
|
5127
|
-
const log = { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() };
|
|
5128
|
-
const lines = text2.split("\n");
|
|
5129
|
-
lines.forEach((line, index) => {
|
|
5130
|
-
const record = line ? parseRecord(line) : null;
|
|
5131
|
-
if (record) applyRecord(log, record);
|
|
5132
|
-
else if (line && index < lines.length - 1) onUnreadable(index + 1);
|
|
5133
|
-
});
|
|
5134
|
-
return log;
|
|
5135
|
-
}
|
|
5136
|
-
function applyRecord(log, record) {
|
|
5137
|
-
if (record.type === "baseline") log.baseline = new Map(Object.entries(record.files));
|
|
5138
|
-
else if (record.type === "entry") log.entries.push(record.entry);
|
|
5139
|
-
else if (record.pinned) log.pins.add(record.id);
|
|
5140
|
-
else log.pins.delete(record.id);
|
|
5141
|
-
}
|
|
5142
|
-
function saveRecord(file, log, record) {
|
|
5143
|
-
let fd;
|
|
5144
|
-
try {
|
|
5145
|
-
fd = openSync5(file, constants3.O_WRONLY | constants3.O_APPEND);
|
|
5146
|
-
} catch (error) {
|
|
5147
|
-
if (error.code !== "ENOENT") throw error;
|
|
5148
|
-
return writeLog(file, log);
|
|
5149
|
-
}
|
|
5150
|
-
try {
|
|
5151
|
-
writeSync2(fd, `${JSON.stringify(record)}
|
|
5152
|
-
`);
|
|
5153
|
-
} finally {
|
|
5154
|
-
closeSync5(fd);
|
|
5155
|
-
}
|
|
5156
|
-
}
|
|
5157
|
-
function writeLog(file, log) {
|
|
5158
|
-
const records = [
|
|
5159
|
-
{ type: "baseline", files: Object.fromEntries(log.baseline) },
|
|
5160
|
-
...log.entries.map((entry) => ({ type: "entry", entry })),
|
|
5161
|
-
...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
|
|
5162
|
-
];
|
|
5163
|
-
mkdirSync7(dirname5(file), { recursive: true });
|
|
5164
|
-
replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
|
|
5165
|
-
`).join(""), 420);
|
|
5166
|
-
}
|
|
5167
|
-
function applyEntry(manifest, entry) {
|
|
5168
|
-
for (const file of entry.files)
|
|
5169
|
-
if (file.after === null) manifest.delete(file.path);
|
|
5170
|
-
else manifest.set(file.path, file.after);
|
|
5171
|
-
}
|
|
5172
|
-
function manifestAt(log, point) {
|
|
5173
|
-
const manifest = new Map(log.baseline);
|
|
5174
|
-
if (point === START) return manifest;
|
|
5175
|
-
for (const entry of log.entries) {
|
|
5176
|
-
applyEntry(manifest, entry);
|
|
5177
|
-
if (entry.id === point) return manifest;
|
|
5178
|
-
}
|
|
5179
|
-
return null;
|
|
5180
|
-
}
|
|
5181
|
-
function undoneIds(entries) {
|
|
5182
|
-
const undone = /* @__PURE__ */ new Set();
|
|
5183
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
5184
|
-
const entry = entries[i];
|
|
5185
|
-
if (entry.undoes && !undone.has(entry.id)) undone.add(entry.undoes);
|
|
5186
|
-
}
|
|
5187
|
-
return undone;
|
|
5188
|
-
}
|
|
5189
|
-
function stepTarget(entries, direction) {
|
|
5190
|
-
const undone = undoneIds(entries);
|
|
5191
|
-
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
5192
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
5193
|
-
const entry = entries[i];
|
|
5194
|
-
const isChange = !entry.undoes;
|
|
5195
|
-
if (direction === "back" && isChange && !undone.has(entry.id)) return entry;
|
|
5196
|
-
if (direction !== "forward") continue;
|
|
5197
|
-
if (isChange) return void 0;
|
|
5198
|
-
const target = byId.get(entry.undoes);
|
|
5199
|
-
if (!undone.has(entry.id) && target && !target.undoes) return entry;
|
|
5200
|
-
}
|
|
5201
|
-
return void 0;
|
|
5202
|
-
}
|
|
5203
|
-
function foldOldest(log) {
|
|
5204
|
-
const oldest = log.entries[0];
|
|
5205
|
-
if (!oldest || log.pins.has(oldest.id)) return false;
|
|
5206
|
-
applyEntry(log.baseline, oldest);
|
|
5207
|
-
log.entries.shift();
|
|
5208
|
-
return true;
|
|
5209
|
-
}
|
|
5210
|
-
function referencedHashes(log, current) {
|
|
5211
|
-
const keep = /* @__PURE__ */ new Set([...log.baseline.values(), ...current.values()]);
|
|
5212
|
-
for (const entry of log.entries)
|
|
5213
|
-
for (const file of entry.files) {
|
|
5214
|
-
if (file.before) keep.add(file.before);
|
|
5215
|
-
if (file.after) keep.add(file.after);
|
|
5216
|
-
}
|
|
5217
|
-
return keep;
|
|
5218
|
-
}
|
|
5219
|
-
|
|
5220
5177
|
// src/routes/history.ts
|
|
5221
5178
|
var YOU = { kind: "person", name: "You" };
|
|
5222
5179
|
var MAX_WINDOW_IDLE_MS = 10 * 6e4;
|
|
@@ -5229,12 +5186,23 @@ async function bodyOf(c) {
|
|
|
5229
5186
|
return body && typeof body === "object" ? body : {};
|
|
5230
5187
|
}
|
|
5231
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
|
+
}
|
|
5232
5193
|
function idleOf(body) {
|
|
5233
5194
|
const idleMs = body.idleMs;
|
|
5234
5195
|
return typeof idleMs === "number" && idleMs > 0 ? Math.min(idleMs, MAX_WINDOW_IDLE_MS) : void 0;
|
|
5235
5196
|
}
|
|
5236
|
-
function
|
|
5237
|
-
|
|
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);
|
|
5238
5206
|
if (!target) return null;
|
|
5239
5207
|
const paths = target.files.map((file) => file.path);
|
|
5240
5208
|
return { id: target.id, label: target.label, endedAt: target.endedAt, paths };
|
|
@@ -5255,7 +5223,7 @@ function registerHistoryRoutes(api, adapter) {
|
|
|
5255
5223
|
base,
|
|
5256
5224
|
(c) => withHistory(adapter, c, (history) => {
|
|
5257
5225
|
const entries = history.list();
|
|
5258
|
-
return { entries, back: nextStep(
|
|
5226
|
+
return { entries, back: nextStep(history, "back"), forward: nextStep(history, "forward") };
|
|
5259
5227
|
})
|
|
5260
5228
|
);
|
|
5261
5229
|
api.post(
|
|
@@ -5263,19 +5231,23 @@ function registerHistoryRoutes(api, adapter) {
|
|
|
5263
5231
|
(c) => withHistory(
|
|
5264
5232
|
adapter,
|
|
5265
5233
|
c,
|
|
5266
|
-
(history, body) => history.step(body.direction === "forward" ? "forward" : "back", YOU)
|
|
5234
|
+
(history, body) => history.step(body.direction === "forward" ? "forward" : "back", YOU, writing(c))
|
|
5267
5235
|
)
|
|
5268
5236
|
);
|
|
5269
5237
|
api.post(
|
|
5270
5238
|
`${base}/undo`,
|
|
5271
5239
|
(c) => withHistory(adapter, c, (history, body) => {
|
|
5272
5240
|
const mode = body.mode === "just-this" || body.mode === "back-to-before" ? body.mode : void 0;
|
|
5273
|
-
return history.undo(text(body.entryId) ?? "", { who: YOU, mode });
|
|
5241
|
+
return history.undo(text(body.entryId) ?? "", { who: YOU, mode, ...writing(c) });
|
|
5274
5242
|
})
|
|
5275
5243
|
);
|
|
5276
5244
|
api.post(
|
|
5277
5245
|
`${base}/restore`,
|
|
5278
|
-
(c) => withHistory(
|
|
5246
|
+
(c) => withHistory(
|
|
5247
|
+
adapter,
|
|
5248
|
+
c,
|
|
5249
|
+
(history, body) => history.restore(text(body.point) ?? "", YOU, writing(c))
|
|
5250
|
+
)
|
|
5279
5251
|
);
|
|
5280
5252
|
api.get(
|
|
5281
5253
|
`${base}/peek/:point`,
|
|
@@ -5294,9 +5266,11 @@ function registerHistoryRoutes(api, adapter) {
|
|
|
5294
5266
|
const paths = Array.isArray(body.paths) ? body.paths.filter((path) => text(path)) : [];
|
|
5295
5267
|
const coalesceKey = text(body.coalesceKey) ?? void 0;
|
|
5296
5268
|
const idleMs = idleOf(body);
|
|
5269
|
+
const overwrote = versionsOf(body.overwrote);
|
|
5297
5270
|
const claimed = await history.claim(YOU, text(body.label) ?? "Edited in Studio", paths, {
|
|
5298
5271
|
...coalesceKey && { coalesceKey },
|
|
5299
|
-
...idleMs && { idleMs }
|
|
5272
|
+
...idleMs && { idleMs },
|
|
5273
|
+
...overwrote && { overwrote }
|
|
5300
5274
|
});
|
|
5301
5275
|
return { claimed };
|
|
5302
5276
|
})
|
|
@@ -5346,18 +5320,20 @@ function createStudioApi(adapter) {
|
|
|
5346
5320
|
}
|
|
5347
5321
|
|
|
5348
5322
|
// src/history/projectHistory.ts
|
|
5349
|
-
import { randomUUID as
|
|
5323
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
5350
5324
|
import { rm as rm2 } from "fs/promises";
|
|
5351
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
5352
|
-
import {
|
|
5325
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync18 } from "fs";
|
|
5326
|
+
import { homedir as homedir2 } from "os";
|
|
5327
|
+
import { join as join19, relative as relative4, resolve as resolve7, sep as sep3 } from "path";
|
|
5353
5328
|
|
|
5354
5329
|
// src/history/blobStore.ts
|
|
5355
|
-
import { createHash as
|
|
5356
|
-
import { constants as
|
|
5330
|
+
import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
|
|
5331
|
+
import { constants as constants3, createReadStream as createReadStream2 } from "fs";
|
|
5357
5332
|
import { copyFile, mkdir, readdir, readFile as readFile2, rename, rm, stat } from "fs/promises";
|
|
5358
|
-
import { dirname as dirname6, join as
|
|
5333
|
+
import { dirname as dirname6, join as join16 } from "path";
|
|
5334
|
+
var BLOB_HASH = /^[0-9a-f]{64}$/;
|
|
5359
5335
|
async function hashFile(path) {
|
|
5360
|
-
const hash =
|
|
5336
|
+
const hash = createHash6("sha256");
|
|
5361
5337
|
for await (const chunk of createReadStream2(path)) hash.update(chunk);
|
|
5362
5338
|
return hash.digest("hex");
|
|
5363
5339
|
}
|
|
@@ -5365,7 +5341,7 @@ async function cloneOrCopy(from, to) {
|
|
|
5365
5341
|
await mkdir(dirname6(to), { recursive: true });
|
|
5366
5342
|
const temp = `${to}.${randomUUID5()}.tmp`;
|
|
5367
5343
|
try {
|
|
5368
|
-
await copyFile(from, temp,
|
|
5344
|
+
await copyFile(from, temp, constants3.COPYFILE_FICLONE);
|
|
5369
5345
|
await rename(temp, to);
|
|
5370
5346
|
} catch (error) {
|
|
5371
5347
|
await rm(temp, { force: true });
|
|
@@ -5376,15 +5352,18 @@ async function openBlobStore(dir) {
|
|
|
5376
5352
|
const sizes = /* @__PURE__ */ new Map();
|
|
5377
5353
|
await mkdir(dir, { recursive: true });
|
|
5378
5354
|
for (const shard of await readdir(dir))
|
|
5379
|
-
for (const hash of await readdir(
|
|
5380
|
-
sizes.set(hash, (await stat(
|
|
5381
|
-
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
|
+
};
|
|
5382
5361
|
let total = [...sizes.values()].reduce((sum, size) => sum + size, 0);
|
|
5383
5362
|
return {
|
|
5384
5363
|
async put(absPath) {
|
|
5385
|
-
const temp =
|
|
5364
|
+
const temp = join16(dir, `incoming-${randomUUID5()}`);
|
|
5386
5365
|
await mkdir(dir, { recursive: true });
|
|
5387
|
-
await copyFile(absPath, temp,
|
|
5366
|
+
await copyFile(absPath, temp, constants3.COPYFILE_FICLONE);
|
|
5388
5367
|
const hash = await hashFile(temp);
|
|
5389
5368
|
if (sizes.has(hash)) {
|
|
5390
5369
|
await rm(temp, { force: true });
|
|
@@ -5397,8 +5376,9 @@ async function openBlobStore(dir) {
|
|
|
5397
5376
|
total += size;
|
|
5398
5377
|
return hash;
|
|
5399
5378
|
},
|
|
5400
|
-
|
|
5401
|
-
|
|
5379
|
+
has: (hash) => sizes.has(hash),
|
|
5380
|
+
read: async (hash) => readFile2(pathOf(hash)),
|
|
5381
|
+
writeTo: async (hash, absPath) => cloneOrCopy(pathOf(hash), absPath),
|
|
5402
5382
|
bytes: () => total,
|
|
5403
5383
|
size: (hash) => sizes.get(hash) ?? 0,
|
|
5404
5384
|
async prune(keep) {
|
|
@@ -5414,13 +5394,13 @@ async function openBlobStore(dir) {
|
|
|
5414
5394
|
|
|
5415
5395
|
// src/history/historyId.ts
|
|
5416
5396
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
5417
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as
|
|
5418
|
-
import { join as
|
|
5419
|
-
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");
|
|
5420
5400
|
var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
5421
5401
|
function readId(projectDir) {
|
|
5422
5402
|
try {
|
|
5423
|
-
const id =
|
|
5403
|
+
const id = readFileSync15(join17(projectDir, ID_PATH), "utf-8").trim();
|
|
5424
5404
|
return ID_SHAPE.test(id) ? id : null;
|
|
5425
5405
|
} catch {
|
|
5426
5406
|
return null;
|
|
@@ -5428,7 +5408,7 @@ function readId(projectDir) {
|
|
|
5428
5408
|
}
|
|
5429
5409
|
function recordedDir(historyDir) {
|
|
5430
5410
|
try {
|
|
5431
|
-
return JSON.parse(
|
|
5411
|
+
return JSON.parse(readFileSync15(join17(historyDir, "project.json"), "utf-8")).dir ?? null;
|
|
5432
5412
|
} catch {
|
|
5433
5413
|
return null;
|
|
5434
5414
|
}
|
|
@@ -5436,23 +5416,225 @@ function recordedDir(historyDir) {
|
|
|
5436
5416
|
function projectHistoryId(projectDir, historyRoot) {
|
|
5437
5417
|
const dir = resolve6(projectDir);
|
|
5438
5418
|
let id = readId(dir);
|
|
5439
|
-
const was = id && recordedDir(
|
|
5419
|
+
const was = id && recordedDir(join17(historyRoot, id));
|
|
5440
5420
|
if (!id || was && was !== dir && existsSync11(was) && readId(was) === id) {
|
|
5441
5421
|
id = randomUUID6();
|
|
5442
|
-
mkdirSync8(
|
|
5443
|
-
|
|
5422
|
+
mkdirSync8(join17(dir, ".hyperframes"), { recursive: true });
|
|
5423
|
+
writeFileSync7(join17(dir, ID_PATH), `${id}
|
|
5444
5424
|
`);
|
|
5445
5425
|
}
|
|
5446
|
-
mkdirSync8(
|
|
5447
|
-
|
|
5426
|
+
mkdirSync8(join17(historyRoot, id), { recursive: true });
|
|
5427
|
+
writeFileSync7(join17(historyRoot, id, "project.json"), JSON.stringify({ dir }));
|
|
5448
5428
|
return id;
|
|
5449
5429
|
}
|
|
5450
5430
|
|
|
5431
|
+
// src/history/ownerLock.ts
|
|
5432
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
5433
|
+
import { linkSync, mkdirSync as mkdirSync9, readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
5434
|
+
import { join as join18 } from "path";
|
|
5435
|
+
var HistoryBusyError = class extends Error {
|
|
5436
|
+
constructor(pid) {
|
|
5437
|
+
super(`This project's history is open in another process (pid ${pid}).`);
|
|
5438
|
+
this.pid = pid;
|
|
5439
|
+
this.name = "HistoryBusyError";
|
|
5440
|
+
}
|
|
5441
|
+
pid;
|
|
5442
|
+
};
|
|
5443
|
+
function alive(pid) {
|
|
5444
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
5445
|
+
try {
|
|
5446
|
+
process.kill(pid, 0);
|
|
5447
|
+
return true;
|
|
5448
|
+
} catch (error) {
|
|
5449
|
+
return error.code === "EPERM";
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
function ownerOf(file) {
|
|
5453
|
+
try {
|
|
5454
|
+
const text2 = readFileSync16(file, "utf-8");
|
|
5455
|
+
return /^\d+$/.test(text2) ? Number(text2) : Number.NaN;
|
|
5456
|
+
} catch (error) {
|
|
5457
|
+
if (error.code === "ENOENT") return null;
|
|
5458
|
+
throw error;
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
function claim(file) {
|
|
5462
|
+
const draft = `${file}-${randomUUID7()}.tmp`;
|
|
5463
|
+
writeFileSync8(draft, String(process.pid));
|
|
5464
|
+
try {
|
|
5465
|
+
linkSync(draft, file);
|
|
5466
|
+
return true;
|
|
5467
|
+
} catch (error) {
|
|
5468
|
+
if (error.code !== "EEXIST") throw error;
|
|
5469
|
+
return false;
|
|
5470
|
+
} finally {
|
|
5471
|
+
rmSync5(draft, { force: true });
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
function releaseOwn(file) {
|
|
5475
|
+
if (ownerOf(file) === process.pid) rmSync5(file, { force: true });
|
|
5476
|
+
}
|
|
5477
|
+
function evictDeadOwner(file) {
|
|
5478
|
+
const evictor = `${file}.evict`;
|
|
5479
|
+
if (!claim(evictor)) {
|
|
5480
|
+
const holder = ownerOf(evictor);
|
|
5481
|
+
if (holder !== null && !alive(holder)) rmSync5(evictor, { force: true });
|
|
5482
|
+
return false;
|
|
5483
|
+
}
|
|
5484
|
+
try {
|
|
5485
|
+
const pid = ownerOf(file);
|
|
5486
|
+
if (pid !== null && !alive(pid)) rmSync5(file, { force: true });
|
|
5487
|
+
return true;
|
|
5488
|
+
} finally {
|
|
5489
|
+
releaseOwn(evictor);
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
async function takeHistoryOwnership(home, waitMs) {
|
|
5493
|
+
const file = join18(home, "owner.pid");
|
|
5494
|
+
const deadline = Date.now() + waitMs;
|
|
5495
|
+
mkdirSync9(home, { recursive: true });
|
|
5496
|
+
for (; ; ) {
|
|
5497
|
+
if (claim(file)) {
|
|
5498
|
+
let held = true;
|
|
5499
|
+
return () => {
|
|
5500
|
+
if (held) releaseOwn(file);
|
|
5501
|
+
held = false;
|
|
5502
|
+
};
|
|
5503
|
+
}
|
|
5504
|
+
const pid = ownerOf(file);
|
|
5505
|
+
if (pid === null) continue;
|
|
5506
|
+
if (!alive(pid) && evictDeadOwner(file)) continue;
|
|
5507
|
+
if (Date.now() >= deadline) throw new HistoryBusyError(pid);
|
|
5508
|
+
await new Promise((settle) => setTimeout(settle, 50));
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
|
|
5512
|
+
// src/history/historyLog.ts
|
|
5513
|
+
import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync10, openSync as openSync5, readFileSync as readFileSync17, writeSync as writeSync2 } from "fs";
|
|
5514
|
+
import { dirname as dirname7 } from "path";
|
|
5515
|
+
var START = "start";
|
|
5516
|
+
function parseRecord(line) {
|
|
5517
|
+
try {
|
|
5518
|
+
return JSON.parse(line);
|
|
5519
|
+
} catch {
|
|
5520
|
+
return null;
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
function readLog(file, onUnreadable) {
|
|
5524
|
+
let text2;
|
|
5525
|
+
try {
|
|
5526
|
+
text2 = readFileSync17(file, "utf-8");
|
|
5527
|
+
} catch {
|
|
5528
|
+
return null;
|
|
5529
|
+
}
|
|
5530
|
+
const log = { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() };
|
|
5531
|
+
const lines = text2.split("\n");
|
|
5532
|
+
lines.forEach((line, index) => {
|
|
5533
|
+
const record = line ? parseRecord(line) : null;
|
|
5534
|
+
if (record) applyRecord(log, record);
|
|
5535
|
+
else if (line && index < lines.length - 1) onUnreadable(index + 1);
|
|
5536
|
+
});
|
|
5537
|
+
return log;
|
|
5538
|
+
}
|
|
5539
|
+
function applyRecord(log, record) {
|
|
5540
|
+
if (record.type === "baseline") log.baseline = new Map(Object.entries(record.files));
|
|
5541
|
+
else if (record.type === "entry") log.entries.push(record.entry);
|
|
5542
|
+
else if (record.pinned) log.pins.add(record.id);
|
|
5543
|
+
else log.pins.delete(record.id);
|
|
5544
|
+
}
|
|
5545
|
+
function saveRecord(file, log, record) {
|
|
5546
|
+
let fd;
|
|
5547
|
+
try {
|
|
5548
|
+
fd = openSync5(file, constants4.O_WRONLY | constants4.O_APPEND);
|
|
5549
|
+
} catch (error) {
|
|
5550
|
+
if (error.code !== "ENOENT") throw error;
|
|
5551
|
+
return writeLog(file, log);
|
|
5552
|
+
}
|
|
5553
|
+
try {
|
|
5554
|
+
writeSync2(fd, `${JSON.stringify(record)}
|
|
5555
|
+
`);
|
|
5556
|
+
} finally {
|
|
5557
|
+
closeSync5(fd);
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
function writeLog(file, log) {
|
|
5561
|
+
const records = [
|
|
5562
|
+
{ type: "baseline", files: Object.fromEntries(log.baseline) },
|
|
5563
|
+
...log.entries.map((entry) => ({ type: "entry", entry })),
|
|
5564
|
+
...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
|
|
5565
|
+
];
|
|
5566
|
+
mkdirSync10(dirname7(file), { recursive: true });
|
|
5567
|
+
replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
|
|
5568
|
+
`).join(""), 420);
|
|
5569
|
+
}
|
|
5570
|
+
function applyEntry(manifest, entry) {
|
|
5571
|
+
for (const file of entry.files)
|
|
5572
|
+
if (file.after === null) manifest.delete(file.path);
|
|
5573
|
+
else manifest.set(file.path, file.after);
|
|
5574
|
+
}
|
|
5575
|
+
function manifestAt(log, point) {
|
|
5576
|
+
const manifest = new Map(log.baseline);
|
|
5577
|
+
if (point === START) return manifest;
|
|
5578
|
+
for (const entry of log.entries) {
|
|
5579
|
+
applyEntry(manifest, entry);
|
|
5580
|
+
if (entry.id === point) return manifest;
|
|
5581
|
+
}
|
|
5582
|
+
return null;
|
|
5583
|
+
}
|
|
5584
|
+
function undoneIds(entries) {
|
|
5585
|
+
const undone = /* @__PURE__ */ new Set();
|
|
5586
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
5587
|
+
const entry = entries[i];
|
|
5588
|
+
if (entry.undoes && !undone.has(entry.id)) undone.add(entry.undoes);
|
|
5589
|
+
}
|
|
5590
|
+
return undone;
|
|
5591
|
+
}
|
|
5592
|
+
function stepTarget(entries, direction) {
|
|
5593
|
+
const undone = undoneIds(entries);
|
|
5594
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
5595
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
5596
|
+
const entry = entries[i];
|
|
5597
|
+
const isChange = !entry.undoes;
|
|
5598
|
+
if (direction === "back" && isChange && !undone.has(entry.id)) return entry;
|
|
5599
|
+
if (direction !== "forward") continue;
|
|
5600
|
+
if (isChange) return void 0;
|
|
5601
|
+
const target = byId.get(entry.undoes);
|
|
5602
|
+
if (!undone.has(entry.id) && target && !target.undoes) return entry;
|
|
5603
|
+
}
|
|
5604
|
+
return void 0;
|
|
5605
|
+
}
|
|
5606
|
+
function foldOldest(log) {
|
|
5607
|
+
const oldest = log.entries[0];
|
|
5608
|
+
if (!oldest || log.pins.has(oldest.id)) return false;
|
|
5609
|
+
applyEntry(log.baseline, oldest);
|
|
5610
|
+
log.entries.shift();
|
|
5611
|
+
return true;
|
|
5612
|
+
}
|
|
5613
|
+
function referencedHashes(log, current) {
|
|
5614
|
+
const keep = /* @__PURE__ */ new Set([...log.baseline.values(), ...current.values()]);
|
|
5615
|
+
for (const entry of log.entries)
|
|
5616
|
+
for (const file of entry.files) {
|
|
5617
|
+
if (file.before) keep.add(file.before);
|
|
5618
|
+
if (file.after) keep.add(file.after);
|
|
5619
|
+
}
|
|
5620
|
+
return keep;
|
|
5621
|
+
}
|
|
5622
|
+
|
|
5451
5623
|
// src/history/projectHistory.ts
|
|
5624
|
+
var DEFAULT_HISTORY_ROOT = join19(homedir2(), ".cache", "hyperframes", "history");
|
|
5452
5625
|
var OUTSIDE = { kind: "outside", name: "Outside" };
|
|
5453
5626
|
var OUTSIDE_LABEL = "Changed outside the app";
|
|
5454
5627
|
var RACY_MS = 2e3;
|
|
5455
5628
|
var statKey = (file, sweptAt) => sweptAt - Math.max(file.mtimeMs, file.ctimeMs) < RACY_MS ? "" : `${file.size}:${file.mtimeMs}:${file.ctimeMs}`;
|
|
5629
|
+
var sameWho = (a, b) => a.kind === b.kind && a.name === b.name;
|
|
5630
|
+
function splitAt(change, at, kept) {
|
|
5631
|
+
if (at === change.after) return [change, null];
|
|
5632
|
+
if (!at || at === change.before || !kept) return [null, change];
|
|
5633
|
+
return [
|
|
5634
|
+
{ ...change, after: at },
|
|
5635
|
+
{ ...change, before: at }
|
|
5636
|
+
];
|
|
5637
|
+
}
|
|
5456
5638
|
function addChange(group, path, before, after) {
|
|
5457
5639
|
const earlier = group.changes.get(path);
|
|
5458
5640
|
const from = earlier ? earlier.before : before;
|
|
@@ -5465,7 +5647,7 @@ var Engine = class {
|
|
|
5465
5647
|
this.projectId = projectId;
|
|
5466
5648
|
this.blobs = blobs;
|
|
5467
5649
|
this.dir = resolve7(options.projectDir);
|
|
5468
|
-
this.home =
|
|
5650
|
+
this.home = join19(options.historyRoot, projectId);
|
|
5469
5651
|
}
|
|
5470
5652
|
options;
|
|
5471
5653
|
projectId;
|
|
@@ -5478,13 +5660,14 @@ var Engine = class {
|
|
|
5478
5660
|
outside = null;
|
|
5479
5661
|
/** A coalescing claim, open until another key, its idle timer, or an operation commits it. */
|
|
5480
5662
|
claimed = null;
|
|
5663
|
+
writeToken;
|
|
5481
5664
|
quietTimer;
|
|
5482
5665
|
maxTimer;
|
|
5483
5666
|
notedTimer = null;
|
|
5484
5667
|
listeners = /* @__PURE__ */ new Set();
|
|
5485
5668
|
tail = Promise.resolve();
|
|
5486
5669
|
get logFile() {
|
|
5487
|
-
return
|
|
5670
|
+
return join19(this.home, "log.jsonl");
|
|
5488
5671
|
}
|
|
5489
5672
|
now() {
|
|
5490
5673
|
return (this.options.now ?? Date.now)();
|
|
@@ -5517,7 +5700,7 @@ var Engine = class {
|
|
|
5517
5700
|
const sweptAt = Date.now();
|
|
5518
5701
|
for (const file of listProjectFiles(this.dir))
|
|
5519
5702
|
this.tracked.set(file.path, {
|
|
5520
|
-
hash: await this.blobs.put(
|
|
5703
|
+
hash: await this.blobs.put(join19(this.dir, file.path)),
|
|
5521
5704
|
stat: statKey(file, sweptAt)
|
|
5522
5705
|
});
|
|
5523
5706
|
this.log.baseline = this.manifest();
|
|
@@ -5529,7 +5712,7 @@ var Engine = class {
|
|
|
5529
5712
|
}
|
|
5530
5713
|
readStatCache() {
|
|
5531
5714
|
try {
|
|
5532
|
-
const files = JSON.parse(
|
|
5715
|
+
const files = JSON.parse(readFileSync18(join19(this.home, "stat.json"), "utf-8"));
|
|
5533
5716
|
return new Map(Object.entries(files));
|
|
5534
5717
|
} catch {
|
|
5535
5718
|
return /* @__PURE__ */ new Map();
|
|
@@ -5537,8 +5720,8 @@ var Engine = class {
|
|
|
5537
5720
|
}
|
|
5538
5721
|
saveStatCache() {
|
|
5539
5722
|
const files = JSON.stringify(Object.fromEntries(this.tracked));
|
|
5540
|
-
|
|
5541
|
-
replaceFileAtomically(
|
|
5723
|
+
mkdirSync11(this.home, { recursive: true });
|
|
5724
|
+
replaceFileAtomically(join19(this.home, "stat.json"), files, 420);
|
|
5542
5725
|
}
|
|
5543
5726
|
/**
|
|
5544
5727
|
* Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
|
|
@@ -5563,7 +5746,7 @@ var Engine = class {
|
|
|
5563
5746
|
/** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
|
|
5564
5747
|
async storeIfPresent(path) {
|
|
5565
5748
|
try {
|
|
5566
|
-
return await this.blobs.put(
|
|
5749
|
+
return await this.blobs.put(join19(this.dir, path));
|
|
5567
5750
|
} catch (error) {
|
|
5568
5751
|
if (error.code === "ENOENT") return null;
|
|
5569
5752
|
throw error;
|
|
@@ -5583,37 +5766,80 @@ var Engine = class {
|
|
|
5583
5766
|
if (window) this.touch(window);
|
|
5584
5767
|
addChange(window ?? this.outsideGroup(), path, before, after);
|
|
5585
5768
|
}
|
|
5586
|
-
/**
|
|
5587
|
-
|
|
5588
|
-
* between Studio's write and its claim (milliseconds) folds into Studio's entry. Per-write tokens would split them.
|
|
5589
|
-
* A write filed to another writer's open window stays that window's.
|
|
5590
|
-
*/
|
|
5591
|
-
async claimNow(who, label, paths, { coalesceKey, idleMs }) {
|
|
5769
|
+
/** ponytail: another's write between Studio's write and its sweep folds into Studio's entry (per-write tokens). */
|
|
5770
|
+
async claimNow(who, label, paths, { coalesceKey, idleMs, overwrote = {} }) {
|
|
5592
5771
|
await this.sweep();
|
|
5593
|
-
const taken = this.
|
|
5594
|
-
if (!taken.length)
|
|
5595
|
-
|
|
5772
|
+
const { taken, cut } = this.takeClaimed(who, paths, overwrote);
|
|
5773
|
+
if (!taken.length) {
|
|
5774
|
+
if (coalesceKey !== this.claimed?.key) await this.commitClaim();
|
|
5775
|
+
return null;
|
|
5776
|
+
}
|
|
5777
|
+
const held = this.heldFor(coalesceKey, taken);
|
|
5778
|
+
if (!held) await this.commitClaim();
|
|
5779
|
+
await this.commitOlder(cut);
|
|
5780
|
+
const group = held ?? this.newGroup(who, label);
|
|
5596
5781
|
for (const change of taken) addChange(group, change.path, change.before, change.after);
|
|
5597
5782
|
if (coalesceKey) return this.holdClaim(group, coalesceKey, idleMs);
|
|
5598
5783
|
const entry = await this.commit(group);
|
|
5599
5784
|
return entry && { id: entry.id };
|
|
5600
5785
|
}
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
await this.
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
const
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5786
|
+
async commitOlder(cut) {
|
|
5787
|
+
if (this.outside) cut.delete(this.outside);
|
|
5788
|
+
await this.commitOutside();
|
|
5789
|
+
for (const [window, paths] of cut) await this.commitCut(window, paths);
|
|
5790
|
+
}
|
|
5791
|
+
/** The held claim `taken` continues: same key, each file from its own last change, no held file changed since. */
|
|
5792
|
+
heldFor(key, taken) {
|
|
5793
|
+
const group = key && this.claimed?.key === key ? this.claimed.group : null;
|
|
5794
|
+
const after = (path, before) => {
|
|
5795
|
+
const own = group?.changes.get(path);
|
|
5796
|
+
return own ? own.after : before;
|
|
5797
|
+
};
|
|
5798
|
+
const takenPaths = new Set(taken.map((change) => change.path));
|
|
5799
|
+
const untouched = (change) => takenPaths.has(change.path) || (this.tracked.get(change.path)?.hash ?? null) === change.after;
|
|
5800
|
+
return group && taken.every((change) => after(change.path, change.before) === change.before) && [...group.changes.values()].every(untouched) ? group : null;
|
|
5801
|
+
}
|
|
5802
|
+
logPath(path) {
|
|
5803
|
+
return relative4(this.dir, resolve7(this.dir, path)).split(sep3).join("/");
|
|
5804
|
+
}
|
|
5805
|
+
/** Takes `paths`' uncommitted changes from others, cut at what `who` overwrote; `cut`: paths each group keeps. */
|
|
5806
|
+
takeClaimed(who, paths, overwrote) {
|
|
5807
|
+
const wanted = new Set(paths.map((path) => this.logPath(path)));
|
|
5808
|
+
const at = new Map(
|
|
5809
|
+
Object.entries(overwrote).map(([path, version]) => [
|
|
5810
|
+
this.logPath(path),
|
|
5811
|
+
hashOfVersion(version)
|
|
5812
|
+
])
|
|
5613
5813
|
);
|
|
5614
|
-
const
|
|
5615
|
-
|
|
5616
|
-
|
|
5814
|
+
const others = this.windows.filter((open) => !sameWho(open.who, who));
|
|
5815
|
+
const groups = this.outside ? [this.outside, ...others] : others;
|
|
5816
|
+
const taken = [];
|
|
5817
|
+
const cut = /* @__PURE__ */ new Map();
|
|
5818
|
+
for (const group of groups) {
|
|
5819
|
+
for (const change of [...group.changes.values()]) {
|
|
5820
|
+
if (!wanted.has(change.path)) continue;
|
|
5821
|
+
const [kept, claimed] = this.cutOut(group, change, at.get(change.path));
|
|
5822
|
+
if (claimed) taken.push(claimed);
|
|
5823
|
+
if (kept && claimed) cut.set(group, [...cut.get(group) ?? [], change.path]);
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
return { taken, cut };
|
|
5827
|
+
}
|
|
5828
|
+
cutOut(group, change, cut) {
|
|
5829
|
+
const [kept, claimed] = splitAt(change, cut, cut !== void 0 && this.blobs.has(cut));
|
|
5830
|
+
group.changes.delete(change.path);
|
|
5831
|
+
if (kept) group.changes.set(kept.path, kept);
|
|
5832
|
+
return [kept, claimed];
|
|
5833
|
+
}
|
|
5834
|
+
/** Commits the window's cut `paths` as their own entry, other files staying: a window holds one change per file,
|
|
5835
|
+
* so the writer's next write to a cut file would otherwise span the claimer's edit. */
|
|
5836
|
+
async commitCut(window, paths) {
|
|
5837
|
+
const part = { ...this.newGroup(window.who, window.label), startedAt: window.startedAt };
|
|
5838
|
+
for (const path of paths) {
|
|
5839
|
+
part.changes.set(path, window.changes.get(path));
|
|
5840
|
+
window.changes.delete(path);
|
|
5841
|
+
}
|
|
5842
|
+
window.entry = await this.commit(part);
|
|
5617
5843
|
}
|
|
5618
5844
|
holdClaim(group, key, idleMs = this.options.quietMs ?? 2e3) {
|
|
5619
5845
|
clearTimeout(this.claimed?.timer);
|
|
@@ -5645,7 +5871,7 @@ var Engine = class {
|
|
|
5645
5871
|
return this.outside;
|
|
5646
5872
|
}
|
|
5647
5873
|
newGroup(who, label) {
|
|
5648
|
-
return { id:
|
|
5874
|
+
return { id: randomUUID8(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
|
|
5649
5875
|
}
|
|
5650
5876
|
async commitOutside() {
|
|
5651
5877
|
clearTimeout(this.quietTimer);
|
|
@@ -5656,9 +5882,9 @@ var Engine = class {
|
|
|
5656
5882
|
}
|
|
5657
5883
|
async commit(group, extra = {}) {
|
|
5658
5884
|
if (!group.changes.size) return null;
|
|
5659
|
-
const {
|
|
5885
|
+
const { id, who, label, startedAt, changes } = group;
|
|
5660
5886
|
const files = [...changes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
5661
|
-
const entry = {
|
|
5887
|
+
const entry = { id, who, label, startedAt, endedAt: this.now(), files, ...extra };
|
|
5662
5888
|
this.log.entries.push(entry);
|
|
5663
5889
|
try {
|
|
5664
5890
|
saveRecord(this.logFile, this.log, { type: "entry", entry });
|
|
@@ -5732,7 +5958,7 @@ var Engine = class {
|
|
|
5732
5958
|
if (!this.windows.includes(window)) return window.entry ?? null;
|
|
5733
5959
|
clearTimeout(window.idleTimer);
|
|
5734
5960
|
this.windows = this.windows.filter((open) => open !== window);
|
|
5735
|
-
window.entry = await this.commit(window);
|
|
5961
|
+
window.entry = await this.commit(window) ?? window.entry ?? null;
|
|
5736
5962
|
return window.entry;
|
|
5737
5963
|
}
|
|
5738
5964
|
/** Every pending write, open window and outside group, committed: for flush and close. */
|
|
@@ -5748,9 +5974,8 @@ var Engine = class {
|
|
|
5748
5974
|
this.windows.push(group);
|
|
5749
5975
|
try {
|
|
5750
5976
|
for (const [path, hash] of target) {
|
|
5751
|
-
if ((this.tracked.get(path)?.hash ?? null)
|
|
5752
|
-
|
|
5753
|
-
else await this.blobs.writeTo(hash, join17(this.dir, path));
|
|
5977
|
+
if ((this.tracked.get(path)?.hash ?? null) !== hash)
|
|
5978
|
+
await this.writeProjectFile(path, hash);
|
|
5754
5979
|
}
|
|
5755
5980
|
await this.sweep();
|
|
5756
5981
|
} finally {
|
|
@@ -5758,6 +5983,17 @@ var Engine = class {
|
|
|
5758
5983
|
}
|
|
5759
5984
|
return this.commit(group, extra);
|
|
5760
5985
|
}
|
|
5986
|
+
/** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
|
|
5987
|
+
async writeProjectFile(path, hash) {
|
|
5988
|
+
const absPath = join19(this.dir, path);
|
|
5989
|
+
const { writeToken } = this;
|
|
5990
|
+
if (writeToken) {
|
|
5991
|
+
const version = hash === null ? DELETED_VERSION : hashVersion(hash);
|
|
5992
|
+
recordFileWriteReceipt(absPath, { path, version, writeToken });
|
|
5993
|
+
}
|
|
5994
|
+
if (hash === null) await rm2(absPath, { force: true });
|
|
5995
|
+
else await this.blobs.writeTo(hash, absPath);
|
|
5996
|
+
}
|
|
5761
5997
|
entry(id) {
|
|
5762
5998
|
const entry = this.log.entries.find((candidate) => candidate.id === id);
|
|
5763
5999
|
if (!entry) throw new Error("That change is no longer kept in this project's history.");
|
|
@@ -5770,9 +6006,7 @@ var Engine = class {
|
|
|
5770
6006
|
}
|
|
5771
6007
|
async undoNow(id, who, mode) {
|
|
5772
6008
|
const entry = this.entry(id);
|
|
5773
|
-
const changed =
|
|
5774
|
-
(file) => (this.tracked.get(file.path)?.hash ?? null) !== file.after
|
|
5775
|
-
);
|
|
6009
|
+
const changed = this.movedOn(entry);
|
|
5776
6010
|
if (changed.length && !mode) return { ok: false, conflict: this.conflict(entry, changed) };
|
|
5777
6011
|
if (mode === "back-to-before") {
|
|
5778
6012
|
const index = this.log.entries.indexOf(entry);
|
|
@@ -5788,6 +6022,19 @@ var Engine = class {
|
|
|
5788
6022
|
entry: await this.writeAs(who, this.undoLabel(entry), target, { undoes: id })
|
|
5789
6023
|
};
|
|
5790
6024
|
}
|
|
6025
|
+
movedOn(entry) {
|
|
6026
|
+
return entry.files.filter((file) => (this.tracked.get(file.path)?.hash ?? null) !== file.after);
|
|
6027
|
+
}
|
|
6028
|
+
/** Back reverts the newest change in effect, unless its files moved on under an earlier-ending edit (a held drag
|
|
6029
|
+
* committed after an agent's turn): then that edit, if it still applies. */
|
|
6030
|
+
next(direction) {
|
|
6031
|
+
const top = stepTarget(this.log.entries, direction);
|
|
6032
|
+
const moved = new Set(top && direction === "back" ? this.movedOn(top).map((f) => f.path) : []);
|
|
6033
|
+
if (!top || !moved.size) return top;
|
|
6034
|
+
const undone = undoneIds(this.log.entries);
|
|
6035
|
+
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)));
|
|
6036
|
+
return under && !this.movedOn(under).length ? under : top;
|
|
6037
|
+
}
|
|
5791
6038
|
conflict(entry, changed) {
|
|
5792
6039
|
const paths = new Set(changed.map((file) => file.path));
|
|
5793
6040
|
const after = this.log.entries.slice(this.log.entries.indexOf(entry) + 1);
|
|
@@ -5803,6 +6050,18 @@ var Engine = class {
|
|
|
5803
6050
|
for (const [path, hash] of files) target.set(path, hash);
|
|
5804
6051
|
return this.writeAs(who, label, target, { restoredTo: point });
|
|
5805
6052
|
}
|
|
6053
|
+
/** Settles, then runs `task` with its writes labelled `writeToken`. */
|
|
6054
|
+
operation(writeToken, task) {
|
|
6055
|
+
return this.queue(async () => {
|
|
6056
|
+
await this.settle();
|
|
6057
|
+
this.writeToken = writeToken;
|
|
6058
|
+
try {
|
|
6059
|
+
return await task();
|
|
6060
|
+
} finally {
|
|
6061
|
+
this.writeToken = void 0;
|
|
6062
|
+
}
|
|
6063
|
+
});
|
|
6064
|
+
}
|
|
5806
6065
|
pointLabel(point) {
|
|
5807
6066
|
return point === START ? "the start" : this.entry(point).label;
|
|
5808
6067
|
}
|
|
@@ -5820,23 +6079,20 @@ var Engine = class {
|
|
|
5820
6079
|
undone: undone.has(entry.id)
|
|
5821
6080
|
}));
|
|
5822
6081
|
},
|
|
5823
|
-
step: (direction, who) => this.
|
|
5824
|
-
|
|
5825
|
-
const target = stepTarget(this.log.entries, direction);
|
|
6082
|
+
step: (direction, who, { writeToken } = {}) => this.operation(writeToken, async () => {
|
|
6083
|
+
const target = this.next(direction);
|
|
5826
6084
|
return target ? this.undoNow(target.id, who) : { ok: true, entry: null };
|
|
5827
6085
|
}),
|
|
5828
|
-
undo: (id, { who, mode }) => this.
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
await this.settle();
|
|
5834
|
-
return this.restoreNow(point, who, `Restored: ${this.pointLabel(point)}`);
|
|
5835
|
-
}),
|
|
6086
|
+
undo: (id, { who, mode, writeToken }) => this.operation(writeToken, () => this.undoNow(id, who, mode)),
|
|
6087
|
+
restore: (point, who, { writeToken } = {}) => this.operation(
|
|
6088
|
+
writeToken,
|
|
6089
|
+
() => this.restoreNow(point, who, `Restored: ${this.pointLabel(point)}`)
|
|
6090
|
+
),
|
|
5836
6091
|
peek: (point) => {
|
|
5837
6092
|
const files = manifestAt(this.log, point);
|
|
5838
6093
|
return files && Object.fromEntries(files);
|
|
5839
6094
|
},
|
|
6095
|
+
next: (direction) => this.next(direction),
|
|
5840
6096
|
readBlob: (hash) => this.blobs.read(hash),
|
|
5841
6097
|
pin: (id, pinned) => {
|
|
5842
6098
|
this.entry(id);
|
|
@@ -5858,14 +6114,27 @@ var Engine = class {
|
|
|
5858
6114
|
};
|
|
5859
6115
|
async function openProjectHistory(options) {
|
|
5860
6116
|
const projectId = projectHistoryId(options.projectDir, options.historyRoot);
|
|
5861
|
-
const
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
6117
|
+
const release = await takeHistoryOwnership(
|
|
6118
|
+
join19(options.historyRoot, projectId),
|
|
6119
|
+
options.ownerWaitMs ?? 5e3
|
|
6120
|
+
);
|
|
6121
|
+
try {
|
|
6122
|
+
const blobs = await openBlobStore(join19(options.historyRoot, projectId, "blobs"));
|
|
6123
|
+
const engine = new Engine(options, projectId, blobs);
|
|
6124
|
+
await engine.queue(() => engine.start());
|
|
6125
|
+
const api = engine.api();
|
|
6126
|
+
return {
|
|
6127
|
+
...api,
|
|
6128
|
+
close: () => api.close().finally(release)
|
|
6129
|
+
};
|
|
6130
|
+
} catch (error) {
|
|
6131
|
+
release();
|
|
6132
|
+
throw error;
|
|
6133
|
+
}
|
|
5865
6134
|
}
|
|
5866
6135
|
|
|
5867
6136
|
// src/helpers/duplicateElement.ts
|
|
5868
|
-
import { ensureHfIds as
|
|
6137
|
+
import { ensureHfIds as ensureHfIds4 } from "@hyperframes/parsers/hf-ids";
|
|
5869
6138
|
function duplicateElementInHtml(source, target, newId, at) {
|
|
5870
6139
|
const { document, wrappedFragment } = parseSourceDocument(source);
|
|
5871
6140
|
const element = findTargetElement(document, target);
|
|
@@ -5885,7 +6154,7 @@ function duplicateElementInHtml(source, target, newId, at) {
|
|
|
5885
6154
|
clone.setAttribute("data-start", String(at));
|
|
5886
6155
|
element.parentElement.insertBefore(clone, element.nextSibling);
|
|
5887
6156
|
const html = wrappedFragment ? document.body.innerHTML || "" : document.toString();
|
|
5888
|
-
return { html:
|
|
6157
|
+
return { html: ensureHfIds4(html), matched: true, newId: uniqueId };
|
|
5889
6158
|
}
|
|
5890
6159
|
function nextUniqueId(document, newId) {
|
|
5891
6160
|
let uniqueId = newId;
|
|
@@ -5915,6 +6184,35 @@ function numericAttribute(element, name) {
|
|
|
5915
6184
|
return Number.isFinite(value) ? value : null;
|
|
5916
6185
|
}
|
|
5917
6186
|
|
|
6187
|
+
// src/helpers/previewDocumentStore.ts
|
|
6188
|
+
import { createHash as createHash7 } from "crypto";
|
|
6189
|
+
import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync7, rmSync as rmSync6 } from "fs";
|
|
6190
|
+
import { join as join20 } from "path";
|
|
6191
|
+
function createPreviewDocumentStore(dir, salt) {
|
|
6192
|
+
const fileFor = (key) => join20(dir, `${createHash7("sha256").update(`${salt}
|
|
6193
|
+
${key}`).digest("hex").slice(0, 32)}.html`);
|
|
6194
|
+
return {
|
|
6195
|
+
read(key) {
|
|
6196
|
+
try {
|
|
6197
|
+
return readFileSync19(fileFor(key), "utf-8");
|
|
6198
|
+
} catch {
|
|
6199
|
+
return null;
|
|
6200
|
+
}
|
|
6201
|
+
},
|
|
6202
|
+
write(key, html) {
|
|
6203
|
+
try {
|
|
6204
|
+
mkdirSync12(dir, { recursive: true });
|
|
6205
|
+
const file = fileFor(key);
|
|
6206
|
+
replaceFileAtomically(file, html, 420);
|
|
6207
|
+
for (const name of readdirSync7(dir)) {
|
|
6208
|
+
if (join20(dir, name) !== file) rmSync6(join20(dir, name), { force: true });
|
|
6209
|
+
}
|
|
6210
|
+
} catch {
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
};
|
|
6214
|
+
}
|
|
6215
|
+
|
|
5918
6216
|
// src/helpers/thumbnailOutput.ts
|
|
5919
6217
|
function thumbnailDeviceScaleFactor({
|
|
5920
6218
|
width,
|
|
@@ -5983,7 +6281,10 @@ function updateBackgroundRemovalProgress(state, event) {
|
|
|
5983
6281
|
state.avgMsPerFrame = event.avgMsPerFrame;
|
|
5984
6282
|
}
|
|
5985
6283
|
export {
|
|
6284
|
+
DEFAULT_HISTORY_ROOT,
|
|
6285
|
+
DELETED_VERSION,
|
|
5986
6286
|
START as HISTORY_START,
|
|
6287
|
+
HistoryBusyError,
|
|
5987
6288
|
MIME_TYPES,
|
|
5988
6289
|
PREVIEW_BUNDLE_OPTIONS,
|
|
5989
6290
|
STUDIO_MANUAL_EDITS_PATH,
|
|
@@ -5994,6 +6295,7 @@ export {
|
|
|
5994
6295
|
compositionsAffectedBy,
|
|
5995
6296
|
consumeFileWriteReceipt,
|
|
5996
6297
|
createBackgroundRemovalJob,
|
|
6298
|
+
createPreviewDocumentStore,
|
|
5997
6299
|
createProjectSignature,
|
|
5998
6300
|
createStudioApi,
|
|
5999
6301
|
createStudioManualEditsRenderBodyScript,
|
|
@@ -6014,6 +6316,7 @@ export {
|
|
|
6014
6316
|
requestSubPath,
|
|
6015
6317
|
settledFileTag,
|
|
6016
6318
|
splitElementInHtml,
|
|
6319
|
+
stampProjectHfIds,
|
|
6017
6320
|
thumbnailDeviceScaleFactor,
|
|
6018
6321
|
walkDir
|
|
6019
6322
|
};
|