@hyperframes/studio-server 0.8.221 → 0.8.222

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.
@@ -7,7 +7,7 @@ import {
7
7
 
8
8
  // src/history/projectHistory.ts
9
9
  import { randomUUID as randomUUID7 } from "crypto";
10
- import { mkdir as mkdir2, readdir as readdir3, rm as rm3, rmdir, stat as stat2, writeFile } from "fs/promises";
10
+ import { mkdir as mkdir2, readdir as readdir4, rm as rm3, rmdir, stat as stat2, writeFile } from "fs/promises";
11
11
  import {
12
12
  existsSync as existsSync3,
13
13
  lstatSync as lstatSync4,
@@ -358,6 +358,7 @@ async function registerRenderOutput(projectDir, output) {
358
358
  // src/helpers/projectSignature.ts
359
359
  import { createHash as createHash2 } from "crypto";
360
360
  import { lstatSync, readFileSync as readFileSync3, readdirSync } from "fs";
361
+ import { lstat, readFile, readdir } from "fs/promises";
361
362
  import { extname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
362
363
  import { isAtomicTempPath } from "@hyperframes/core/atomic-file";
363
364
  import { isProjectIgnoredDir } from "@hyperframes/core/project-folders";
@@ -408,6 +409,15 @@ function signatureEntryStat(projectDir, entry, file, outputs) {
408
409
  return null;
409
410
  }
410
411
  }
412
+ function signatureFileOf(file, stat4) {
413
+ return {
414
+ file,
415
+ mtimeMs: stat4.mtimeMs,
416
+ ctimeMs: stat4.ctimeMs,
417
+ size: stat4.size,
418
+ textContentEligible: isTextContentEligible(file, stat4.size)
419
+ };
420
+ }
411
421
  function collectProjectSignatureFiles(projectDir, dir, files, outputs = readRenderOutputsOrNone(projectDir)) {
412
422
  let entries;
413
423
  try {
@@ -422,13 +432,7 @@ function collectProjectSignatureFiles(projectDir, dir, files, outputs = readRend
422
432
  if (stat4.isDirectory()) {
423
433
  collectProjectSignatureFiles(projectDir, file, files, outputs);
424
434
  } else if (stat4.isFile()) {
425
- files.push({
426
- file,
427
- mtimeMs: stat4.mtimeMs,
428
- ctimeMs: stat4.ctimeMs,
429
- size: stat4.size,
430
- textContentEligible: isTextContentEligible(file, stat4.size)
431
- });
435
+ files.push(signatureFileOf(file, stat4));
432
436
  }
433
437
  }
434
438
  }
@@ -444,16 +448,40 @@ function collectProjectSignatureManifestFiles(projectDir, files) {
444
448
  continue;
445
449
  }
446
450
  if (stat4.isSymbolicLink() || !stat4.isFile()) continue;
447
- files.push({
448
- file,
449
- mtimeMs: stat4.mtimeMs,
450
- ctimeMs: stat4.ctimeMs,
451
- size: stat4.size,
452
- textContentEligible: isTextContentEligible(file, stat4.size)
453
- });
451
+ files.push(signatureFileOf(file, stat4));
454
452
  seen.add(file);
455
453
  }
456
454
  }
455
+ async function signatureEntryStatAsync(projectDir, entry, file, outputs) {
456
+ if (isSkippedEntry(entry) || !isPathWithin(projectDir, file)) return null;
457
+ if (isRenderOutput(projectDir, file, outputs)) return null;
458
+ try {
459
+ const stat4 = await lstat(file);
460
+ return stat4.isSymbolicLink() ? null : stat4;
461
+ } catch {
462
+ return null;
463
+ }
464
+ }
465
+ async function collectProjectSignatureFilesAsync(projectDir, dir, files, outputs) {
466
+ let entries;
467
+ try {
468
+ entries = (await readdir(dir)).sort();
469
+ } catch {
470
+ return;
471
+ }
472
+ const paths = entries.map((entry) => resolve2(dir, entry));
473
+ const stats = await Promise.all(
474
+ entries.map((entry, i) => signatureEntryStatAsync(projectDir, entry, paths[i], outputs))
475
+ );
476
+ for (const [i, stat4] of stats.entries()) {
477
+ if (!stat4) continue;
478
+ if (stat4.isDirectory()) {
479
+ await collectProjectSignatureFilesAsync(projectDir, paths[i], files, outputs);
480
+ } else if (stat4.isFile()) {
481
+ files.push(signatureFileOf(paths[i], stat4));
482
+ }
483
+ }
484
+ }
457
485
  function createProjectFingerprint(projectDir, files) {
458
486
  const hash = createHash2("sha256");
459
487
  for (const entry of files) {
@@ -476,6 +504,17 @@ function collectProjectFiles(normalizedProjectDir) {
476
504
  collectProjectSignatureManifestFiles(normalizedProjectDir, collected);
477
505
  return collected;
478
506
  }
507
+ async function collectProjectFilesAsync(normalizedProjectDir) {
508
+ const collected = [];
509
+ await collectProjectSignatureFilesAsync(
510
+ normalizedProjectDir,
511
+ normalizedProjectDir,
512
+ collected,
513
+ readRenderOutputsOrNone(normalizedProjectDir)
514
+ );
515
+ collectProjectSignatureManifestFiles(normalizedProjectDir, collected);
516
+ return collected;
517
+ }
479
518
  function listProjectFiles(projectDir) {
480
519
  const normalizedProjectDir = resolve2(projectDir);
481
520
  return collectProjectFiles(normalizedProjectDir).map((entry) => ({
@@ -493,48 +532,79 @@ async function resolveProjectAndSignature(adapter, projectId) {
493
532
  if (!project) return null;
494
533
  return { project, signature: resolveProjectSignature(adapter, project.dir) };
495
534
  }
496
- function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set()) {
497
- const normalizedProjectDir = resolve2(projectDir);
498
- const signedAt = Date.now();
499
- const collected = collectProjectFiles(normalizedProjectDir);
535
+ function planSignature(dir, collected, excluding, signedAt) {
500
536
  const files = collected.filter(
501
- (entry) => !excluding.has(relative2(normalizedProjectDir, entry.file).split(sep2).join("/"))
537
+ (entry) => !excluding.has(relative2(dir, entry.file).split(sep2).join("/"))
502
538
  );
503
539
  files.sort((a, b) => a.file.localeCompare(b.file));
504
- const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
505
- const cacheKey = excluding.size ? `${normalizedProjectDir}\0${createHash2("sha256").update([...excluding].sort().join("\0")).digest("hex")}` : normalizedProjectDir;
506
- const cached = projectSignatureCache.get(cacheKey);
507
- if (cached?.fingerprint === fingerprint) return cached.signature;
540
+ const cacheKey = excluding.size ? `${dir}\0${createHash2("sha256").update([...excluding].sort().join("\0")).digest("hex")}` : dir;
541
+ return { dir, files, cacheKey, fingerprint: createProjectFingerprint(dir, files), signedAt };
542
+ }
543
+ function hashSignatureEntry(hash, dir, entry, content) {
544
+ hash.update(relative2(dir, entry.file));
545
+ hash.update("\0");
546
+ hash.update(String(entry.size));
547
+ hash.update("\0");
548
+ hash.update(content ?? String(entry.mtimeMs));
549
+ hash.update("\0");
550
+ }
551
+ function finishSignature(plan, hash) {
552
+ const signature = hash.digest("hex").slice(0, 24);
553
+ const settledATickBeforeSigning = plan.files.every(
554
+ (entry) => Math.max(entry.mtimeMs, entry.ctimeMs) < plan.signedAt - COARSEST_FILE_TIME_TICK_MS
555
+ );
556
+ if (settledATickBeforeSigning) {
557
+ projectSignatureCache.set(plan.cacheKey, { fingerprint: plan.fingerprint, signature });
558
+ }
559
+ return signature;
560
+ }
561
+ var cachedSignature = (plan) => {
562
+ const cached = projectSignatureCache.get(plan.cacheKey);
563
+ return cached?.fingerprint === plan.fingerprint ? cached.signature : void 0;
564
+ };
565
+ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set()) {
566
+ const dir = resolve2(projectDir);
567
+ const plan = planSignature(dir, collectProjectFiles(dir), excluding, Date.now());
568
+ const cached = cachedSignature(plan);
569
+ if (cached !== void 0) return cached;
508
570
  const hash = createHash2("sha256");
509
- for (const entry of files) {
510
- const relativePath = relative2(normalizedProjectDir, entry.file);
511
- hash.update(relativePath);
512
- hash.update("\0");
513
- hash.update(String(entry.size));
514
- hash.update("\0");
571
+ for (const entry of plan.files) {
572
+ let content = null;
515
573
  if (entry.textContentEligible) {
516
574
  try {
517
- hash.update(readFileSync3(entry.file));
575
+ content = readFileSync3(entry.file);
518
576
  } catch {
519
- hash.update(String(entry.mtimeMs));
577
+ content = null;
520
578
  }
521
- } else {
522
- hash.update(String(entry.mtimeMs));
523
579
  }
524
- hash.update("\0");
580
+ hashSignatureEntry(hash, dir, entry, content);
525
581
  }
526
- const signature = hash.digest("hex").slice(0, 24);
527
- const settledATickBeforeSigning = files.every(
528
- (entry) => Math.max(entry.mtimeMs, entry.ctimeMs) < signedAt - COARSEST_FILE_TIME_TICK_MS
529
- );
530
- if (settledATickBeforeSigning) projectSignatureCache.set(cacheKey, { fingerprint, signature });
531
- return signature;
582
+ return finishSignature(plan, hash);
583
+ }
584
+ var SIGNATURE_READ_BATCH = 32;
585
+ async function createProjectSignatureAsync(projectDir, excluding = /* @__PURE__ */ new Set()) {
586
+ const dir = resolve2(projectDir);
587
+ const signedAt = Date.now();
588
+ const plan = planSignature(dir, await collectProjectFilesAsync(dir), excluding, signedAt);
589
+ const cached = cachedSignature(plan);
590
+ if (cached !== void 0) return cached;
591
+ const hash = createHash2("sha256");
592
+ for (let at = 0; at < plan.files.length; at += SIGNATURE_READ_BATCH) {
593
+ const batch = plan.files.slice(at, at + SIGNATURE_READ_BATCH);
594
+ const contents = await Promise.all(
595
+ batch.map(
596
+ (entry) => entry.textContentEligible ? readFile(entry.file).catch(() => null) : null
597
+ )
598
+ );
599
+ for (const [i, entry] of batch.entries()) hashSignatureEntry(hash, dir, entry, contents[i]);
600
+ }
601
+ return finishSignature(plan, hash);
532
602
  }
533
603
 
534
604
  // src/history/blobStore.ts
535
605
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
536
606
  import { constants, createReadStream, createWriteStream } from "fs";
537
- import { copyFile, mkdir, open, readdir, readFile, rm, stat } from "fs/promises";
607
+ import { copyFile, mkdir, open, readdir as readdir2, readFile as readFile2, rm, stat } from "fs/promises";
538
608
  import { dirname, join as join2 } from "path";
539
609
  import { pipeline } from "stream/promises";
540
610
  import { atomicTempPath } from "@hyperframes/core/atomic-file";
@@ -577,7 +647,7 @@ function blobPath(dir, hash) {
577
647
  if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
578
648
  return join2(dir, hash.slice(0, 2), hash);
579
649
  }
580
- var readBlob = (dir, hash) => readFile(blobPath(dir, hash));
650
+ var readBlob = (dir, hash) => readFile2(blobPath(dir, hash));
581
651
  async function hashFile(path) {
582
652
  const hash = createHash3("sha256");
583
653
  for await (const chunk of createReadStream(path)) hash.update(chunk);
@@ -630,7 +700,7 @@ async function writeInPlace(temp, to, hash) {
630
700
  if (outcome === "mismatch")
631
701
  throw new Error("The file was written in place, but it does not match the copy history kept.");
632
702
  }
633
- var listDir = (path) => readdir(path, { withFileTypes: true }).catch((error) => {
703
+ var listDir = (path) => readdir2(path, { withFileTypes: true }).catch((error) => {
634
704
  if (error.code === "ENOENT") return [];
635
705
  throw error;
636
706
  });
@@ -687,7 +757,7 @@ async function openBlobStore(dir) {
687
757
  },
688
758
  reference: (absPath) => hashFile(absPath),
689
759
  has: (hash) => sizes.has(hash) || asideSizes.has(hash),
690
- read: (hash) => readFile(pathOf(hash)),
760
+ read: (hash) => readFile2(pathOf(hash)),
691
761
  writeTo: async (hash, absPath, beforeReplace) => cloneOrCopy(pathOf(hash), absPath, hash, beforeReplace),
692
762
  async release(held) {
693
763
  for (const [hash, path] of held) {
@@ -751,7 +821,8 @@ var MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
751
821
  ".opus"
752
822
  ]);
753
823
  var COPY_NOTHING_ABOVE_BYTES = 64 * 1024 ** 2;
754
- var isUncopied = (path, size) => size > COPY_NOTHING_ABOVE_BYTES || MEDIA_EXTENSIONS.has(extname2(path).toLowerCase());
824
+ var isUncopiedMedia = (path) => MEDIA_EXTENSIONS.has(extname2(path).toLowerCase());
825
+ var isUncopied = (path, size) => size > COPY_NOTHING_ABOVE_BYTES || isUncopiedMedia(path);
755
826
  function uncopiedOnlyHashes(log, sizeOf) {
756
827
  const copied = /* @__PURE__ */ new Set();
757
828
  const uncopied = /* @__PURE__ */ new Set();
@@ -770,7 +841,7 @@ function uncopiedOnlyHashes(log, sizeOf) {
770
841
  // src/history/pruneHistories.ts
771
842
  import { randomUUID as randomUUID5 } from "crypto";
772
843
  import { readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
773
- import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
844
+ import { lstat as lstat2, readdir as readdir3, rm as rm2 } from "fs/promises";
774
845
  import { tmpdir } from "os";
775
846
  import { dirname as dirname2, join as join4 } from "path";
776
847
 
@@ -966,9 +1037,9 @@ function lastUsed(home) {
966
1037
  }
967
1038
  async function bytesIn(dir) {
968
1039
  let total = 0;
969
- for (const entry of await readdir2(dir, { withFileTypes: true })) {
1040
+ for (const entry of await readdir3(dir, { withFileTypes: true })) {
970
1041
  const path = join4(dir, entry.name);
971
- total += entry.isDirectory() ? await bytesIn(path) : (await lstat(path)).size;
1042
+ total += entry.isDirectory() ? await bytesIn(path) : (await lstat2(path)).size;
972
1043
  }
973
1044
  return total;
974
1045
  }
@@ -1781,7 +1852,7 @@ var isMissingFolder = (error) => ["ENOENT", "ENOTDIR"].includes(error.code ?? ""
1781
1852
  async function removeEmptyFolders(dir) {
1782
1853
  let entries;
1783
1854
  try {
1784
- entries = await readdir3(dir, { withFileTypes: true });
1855
+ entries = await readdir4(dir, { withFileTypes: true });
1785
1856
  } catch (error) {
1786
1857
  if (isMissingFolder(error)) return;
1787
1858
  throw error;
@@ -2653,7 +2724,7 @@ var Engine = class {
2653
2724
  const dest = resolve8(emptyDir);
2654
2725
  if (isWithin(this.dir, dest))
2655
2726
  throw new Error(`Checkout writes outside the project only: ${dest}`);
2656
- if ((await readdir3(dest).catch(missingIsEmpty)).length > 0)
2727
+ if ((await readdir4(dest).catch(missingIsEmpty)).length > 0)
2657
2728
  throw new Error(`Checkout writes into an empty folder only: ${dest}`);
2658
2729
  const files = manifestAround(this.log, entryId, side);
2659
2730
  for (const [path, hash] of files)
@@ -2662,7 +2733,7 @@ var Engine = class {
2662
2733
  try {
2663
2734
  for (const [path, hash] of files) await this.blobs.writeTo(hash, join8(dest, path));
2664
2735
  } catch (error) {
2665
- for (const name of await readdir3(dest).catch(() => []))
2736
+ for (const name of await readdir4(dest).catch(() => []))
2666
2737
  await rm3(join8(dest, name), { recursive: true, force: true });
2667
2738
  throw error;
2668
2739
  }
@@ -2862,6 +2933,7 @@ export {
2862
2933
  resolveProjectSignature,
2863
2934
  resolveProjectAndSignature,
2864
2935
  createProjectSignature,
2936
+ createProjectSignatureAsync,
2865
2937
  fileContentVersion,
2866
2938
  DELETED_VERSION,
2867
2939
  settledFileTag,
@@ -2872,6 +2944,7 @@ export {
2872
2944
  consumeFileWriteReceipt,
2873
2945
  serveWhileOpen,
2874
2946
  onRestoredInPlace,
2947
+ isUncopiedMedia,
2875
2948
  HistoryIdError,
2876
2949
  listProjectHistories,
2877
2950
  pruneGoneProjectHistories,
@@ -13,7 +13,11 @@ import {
13
13
  mintHfId,
14
14
  walkCompositionDescendants
15
15
  } from "@hyperframes/parsers/hf-ids";
16
- import { readClipTiming, writeClipTiming } from "@hyperframes/core/composition-contract";
16
+ import {
17
+ readClipTiming,
18
+ SPLIT_BOUNDARY_EPSILON_S,
19
+ writeClipTiming
20
+ } from "@hyperframes/core/composition-contract";
17
21
  import { relinkSplitHalves } from "@hyperframes/core/media-link";
18
22
 
19
23
  // src/helpers/sourceStyleMutation.ts
@@ -334,7 +338,7 @@ function splitElementInHtml(source, target, splitTime, newId, fallbackTiming) {
334
338
  start = fallbackTiming.start;
335
339
  duration = fallbackTiming.duration;
336
340
  }
337
- if (duration <= 0 || splitTime <= start || splitTime >= start + duration) {
341
+ if (duration <= 0 || splitTime < start + SPLIT_BOUNDARY_EPSILON_S || splitTime > start + duration - SPLIT_BOUNDARY_EPSILON_S) {
338
342
  return { html: source, matched: false, newId: null };
339
343
  }
340
344
  newId = nextUniqueId(document, newId);
@@ -13,7 +13,7 @@ import {
13
13
  splitElementInHtml,
14
14
  unwrapElementsFromHtml,
15
15
  wrapElementsInHtml
16
- } from "../chunk-OF4B2ILS.js";
16
+ } from "../chunk-V6K3WWWU.js";
17
17
  export {
18
18
  dedupeClonedCompositionId,
19
19
  findTargetElement,
@@ -11,6 +11,8 @@ declare function reclaimUncopiedBlobs(historyRoot: string, { dryRun, onError }?:
11
11
  onError?: (error: unknown) => void;
12
12
  }): Promise<ReclaimedHistory[]>;
13
13
 
14
+ declare const isUncopiedMedia: (path: string) => boolean;
15
+
14
16
  declare class HistoryBusyError extends Error {
15
17
  readonly pid: number;
16
18
  readonly url?: string | undefined;
@@ -152,4 +154,4 @@ declare function registerRenderOutput(projectDir: string, output: string): Promi
152
154
  /** Explicit outputs remain user evidence even when reusing a managed UUID folder. */
153
155
  declare function retainSnapshotOutput(project: string, path: string): void;
154
156
 
155
- export { type ArtifactCapture, type ArtifactInput, type ArtifactRecord, HistoryBusyError, HistoryIdError, HistoryTarget, HistoryWho, type InPlaceOutcome, ProjectHistory, type ProjectHistoryRecord, type PrunedHistory, type ReclaimedHistory, type RenderRecord, type VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, listProjectHistories, onRestoredInPlace, pruneGoneProjectHistories, reclaimUncopiedBlobs, recordArtifact, recordArtifacts, recordRenderSuccess, registerRenderOutput, retainSnapshotOutput };
157
+ export { type ArtifactCapture, type ArtifactInput, type ArtifactRecord, HistoryBusyError, HistoryIdError, HistoryTarget, HistoryWho, type InPlaceOutcome, ProjectHistory, type ProjectHistoryRecord, type PrunedHistory, type ReclaimedHistory, type RenderRecord, type VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, isUncopiedMedia, listProjectHistories, onRestoredInPlace, pruneGoneProjectHistories, reclaimUncopiedBlobs, recordArtifact, recordArtifacts, recordRenderSuccess, registerRenderOutput, retainSnapshotOutput };
@@ -12,6 +12,7 @@ import {
12
12
  finishSnapshotRun,
13
13
  forgetRecordedArtifacts,
14
14
  historyCache,
15
+ isUncopiedMedia,
15
16
  listProjectHistories,
16
17
  onRestoredInPlace,
17
18
  openProjectHistory,
@@ -23,7 +24,7 @@ import {
23
24
  recordRenderSuccess,
24
25
  registerRenderOutput,
25
26
  retainSnapshotOutput
26
- } from "../chunk-UMPRW7BV.js";
27
+ } from "../chunk-CZ2QLV2C.js";
27
28
  import "../chunk-QF7MFJJU.js";
28
29
  import "../chunk-SVQFE44C.js";
29
30
  import "../chunk-L3HJVTQE.js";
@@ -41,6 +42,7 @@ export {
41
42
  finishSnapshotRun,
42
43
  forgetRecordedArtifacts,
43
44
  historyCache,
45
+ isUncopiedMedia,
44
46
  listProjectHistories,
45
47
  onRestoredInPlace,
46
48
  openProjectHistory,
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Hono } from 'hono';
2
2
  import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-BnQDzKj1.js';
3
3
  export { L as LintResult, a as PreviewApiAdapter, R as RenderJobState, b as ResolvedProject, c as StudioSelectionResponse, d as StudioSelectionSnapshot, e as StudioSelectionTextField } from './mediaProxyPreview-BnQDzKj1.js';
4
4
  export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, S as HISTORY_START, H as HistoryClosedError, a as HistoryEntry, b as HistoryEntrySide, c as HistoryFileChange, d as HistoryListItem, e as HistoryRefusedError, f as HistoryResult, g as HistoryTarget, h as HistoryWho, i as HistoryWindow, M as MAX_WINDOW_IDLE_MS, P as ProjectHistory, j as ProjectHistoryOptions, k as ProjectHistoryView, U as UNDO_MODES, l as UndoMode, o as openProjectHistory, r as readProjectHistory } from './projectHistory-C18HDvac.js';
5
- export { ArtifactCapture, ArtifactInput, ArtifactRecord, HistoryBusyError, HistoryIdError, InPlaceOutcome, ProjectHistoryRecord, PrunedHistory, ReclaimedHistory, RenderRecord, VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, listProjectHistories, onRestoredInPlace, pruneGoneProjectHistories, reclaimUncopiedBlobs, recordArtifact, recordArtifacts, recordRenderSuccess, registerRenderOutput, retainSnapshotOutput } from './history/index.js';
5
+ export { ArtifactCapture, ArtifactInput, ArtifactRecord, HistoryBusyError, HistoryIdError, InPlaceOutcome, ProjectHistoryRecord, PrunedHistory, ReclaimedHistory, RenderRecord, VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, isUncopiedMedia, listProjectHistories, onRestoredInPlace, pruneGoneProjectHistories, reclaimUncopiedBlobs, recordArtifact, recordArtifacts, recordRenderSuccess, registerRenderOutput, retainSnapshotOutput } from './history/index.js';
6
6
  export { CACHE_DIR_NAME as PROXY_CACHE_DIR_NAME } from './helpers/proxyTranscoder.js';
7
7
  export { walkDir } from './helpers/safePath.js';
8
8
  export { stampProjectHfIds } from './helpers/hfIdPersist.js';
@@ -54,6 +54,8 @@ declare function affectsProjectSignature(projectDir: string, changedPath: string
54
54
  * `excluding` (project-relative paths) leaves those files out.
55
55
  */
56
56
  declare function createProjectSignature(projectDir: string, excluding?: ReadonlySet<string>): string;
57
+ /** `createProjectSignature` without blocking the event loop: the same signature, with the walk and reads awaited. */
58
+ declare function createProjectSignatureAsync(projectDir: string, excluding?: ReadonlySet<string>): Promise<string>;
57
59
 
58
60
  /**
59
61
  * Compositions whose rendered frames a write at `changedPath` can change, or `null` for all
@@ -230,4 +232,4 @@ type BackgroundRemovalRender = (options: {
230
232
  }>;
231
233
  declare function createBackgroundRemovalJob(opts: BackgroundRemovalJobOptions, render: BackgroundRemovalRender): MediaProcessingJobState;
232
234
 
233
- export { type AppliedFileMutation, type BackgroundRemovalRender, DELETED_VERSION, type DuplicateElementResult, type FileMutationInput, type FileWriteReceipt, MAX_UPLOAD_BYTES, MIME_TYPES, MediaProcessingJobState, PREVIEW_BUNDLE_OPTIONS, STUDIO_SIGNATURE_MANIFEST_PATHS, SourceMutationTarget, StudioApiAdapter, type ThumbnailOutputDimensions, affectsPreview, affectsProjectSignature, applyFileMutations, buildSubCompositionHtml, cleanupProxyCache, compositionsAffectedBy, consumeFileWriteReceipt, createBackgroundRemovalJob, createPreviewDocumentStore, createProjectSignature, createStudioApi, duplicateElementInHtml, fileContentVersion, getMimeType, identifyFileWrite, requestSubPath, settledFileTag, thumbnailDeviceScaleFactor };
235
+ export { type AppliedFileMutation, type BackgroundRemovalRender, DELETED_VERSION, type DuplicateElementResult, type FileMutationInput, type FileWriteReceipt, MAX_UPLOAD_BYTES, MIME_TYPES, MediaProcessingJobState, PREVIEW_BUNDLE_OPTIONS, STUDIO_SIGNATURE_MANIFEST_PATHS, SourceMutationTarget, StudioApiAdapter, type ThumbnailOutputDimensions, affectsPreview, affectsProjectSignature, applyFileMutations, buildSubCompositionHtml, cleanupProxyCache, compositionsAffectedBy, consumeFileWriteReceipt, createBackgroundRemovalJob, createPreviewDocumentStore, createProjectSignature, createProjectSignatureAsync, createStudioApi, duplicateElementInHtml, fileContentVersion, getMimeType, identifyFileWrite, requestSubPath, settledFileTag, thumbnailDeviceScaleFactor };
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  splitElementInHtml,
18
18
  unwrapElementsFromHtml,
19
19
  wrapElementsInHtml
20
- } from "./chunk-OF4B2ILS.js";
20
+ } from "./chunk-V6K3WWWU.js";
21
21
  import {
22
22
  isCompositionSource,
23
23
  stampProjectHfIds
@@ -42,12 +42,14 @@ import {
42
42
  clearFileWriteReceipt,
43
43
  consumeFileWriteReceipt,
44
44
  createProjectSignature,
45
+ createProjectSignatureAsync,
45
46
  createWriteToken,
46
47
  fileContentVersion,
47
48
  finishSnapshotRun,
48
49
  forgetRecordedArtifacts,
49
50
  historyCache,
50
51
  identifyFileWrite,
52
+ isUncopiedMedia,
51
53
  listProjectHistories,
52
54
  onRestoredInPlace,
53
55
  openProjectHistory,
@@ -64,7 +66,7 @@ import {
64
66
  retainSnapshotOutput,
65
67
  serveWhileOpen,
66
68
  settledFileTag
67
- } from "./chunk-UMPRW7BV.js";
69
+ } from "./chunk-CZ2QLV2C.js";
68
70
  import {
69
71
  clearElementScreenshotIsolation,
70
72
  getElementScreenshotClip
@@ -351,7 +353,7 @@ function withRootPathsRelative(html, base) {
351
353
  function rewriteRelativePaths(root, compPath, projectDir) {
352
354
  const assetExists = (path) => existsSync(join2(projectDir, path));
353
355
  rewriteAssetPaths(
354
- root.querySelectorAll("[src], [href]"),
356
+ root.querySelectorAll("[src], [href], [srcset]"),
355
357
  compPath,
356
358
  (el, attr) => el.getAttribute(attr),
357
359
  (el, attr, value) => el.setAttribute(attr, value),
@@ -1020,11 +1022,9 @@ function validateUploadedMediaBuffer(fileName, buffer, runner = defaultRunner) {
1020
1022
  // src/helpers/backupJournal.ts
1021
1023
  import { readdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
1022
1024
  import { Buffer as Buffer2 } from "buffer";
1025
+ import { createHash as createHash2 } from "crypto";
1023
1026
  import { join as join5, relative as relative2 } from "path";
1024
1027
  var DEFAULT_KEEP_PER_FILE = 10;
1025
- function backupKeyForPath(path) {
1026
- return Buffer2.from(path, "utf-8").toString("base64url");
1027
- }
1028
1028
  function timestampPrefix() {
1029
1029
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1030
1030
  }
@@ -1041,10 +1041,11 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
1041
1041
  const relativePath = relative2(projectDir, absPath);
1042
1042
  const backupDir = join5(projectDir, ".hyperframes", "backup");
1043
1043
  mkdirWithinProject(projectDir, backupDir);
1044
- const backupKey = backupKeyForPath(relativePath);
1044
+ const pathKey2 = Buffer2.from(relativePath, "utf-8").toString("base64url");
1045
+ const backupKey = pathKey2.length <= 200 ? pathKey2 : `sha256~${createHash2("sha256").update(relativePath).digest("hex")}`;
1045
1046
  const backupPath = nextBackupPath(backupDir, backupKey);
1046
1047
  writeFileSync3(backupPath, content);
1047
- pruneBackups(backupDir, backupKey, options.keepPerFile ?? DEFAULT_KEEP_PER_FILE);
1048
+ pruneBackups(backupDir, [pathKey2, backupKey], options.keepPerFile ?? DEFAULT_KEEP_PER_FILE);
1048
1049
  return { backupPath };
1049
1050
  } catch (error) {
1050
1051
  if (isProjectRootMissing(error)) throw error;
@@ -1071,11 +1072,10 @@ function nextBackupPath(backupDir, backupKey) {
1071
1072
  counter += 1;
1072
1073
  }
1073
1074
  }
1074
- function pruneBackups(backupDir, backupKey, keepPerFile) {
1075
+ function pruneBackups(backupDir, backupKeys, keepPerFile) {
1075
1076
  const keep = Math.max(1, Math.floor(keepPerFile));
1076
- const suffix = `-${backupKey}`;
1077
- const numberedSuffix = new RegExp(`-${backupKey}-\\d+$`);
1078
- const matches = readdirSync(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join5(backupDir, name)).sort((a, b) => {
1077
+ const suffix = new RegExp(`-(?:${backupKeys.join("|")})(?:-\\d+)?$`);
1078
+ const matches = readdirSync(backupDir).filter((name) => suffix.test(name)).map((name) => join5(backupDir, name)).sort((a, b) => {
1079
1079
  return b.localeCompare(a);
1080
1080
  });
1081
1081
  for (const file of matches.slice(keep)) {
@@ -3915,7 +3915,7 @@ import { createReadStream, existsSync as existsSync5, readFileSync as readFileSy
3915
3915
  import { stripSdrPngColourTags } from "@hyperframes/core/compiler/png-colour-tags";
3916
3916
  import { Readable } from "stream";
3917
3917
  import { join as join8, resolve as resolve4 } from "path";
3918
- import { createHash as createHash2 } from "crypto";
3918
+ import { createHash as createHash3 } from "crypto";
3919
3919
  import {
3920
3920
  AFTER_FONTS_SCRIPT_TYPE,
3921
3921
  addScenePartsManifest,
@@ -4315,7 +4315,7 @@ function parsePreviewVariablesParam(raw) {
4315
4315
  }
4316
4316
  function variablesEtagSalt(raw) {
4317
4317
  if (!raw) return "";
4318
- return `:vars:${createHash2("sha1").update(raw).digest("hex").slice(0, 12)}`;
4318
+ return `:vars:${createHash3("sha1").update(raw).digest("hex").slice(0, 12)}`;
4319
4319
  }
4320
4320
  function previewVariablesFromRequest(rawVariables) {
4321
4321
  const parse2 = parsePreviewVariablesParam(rawVariables);
@@ -4412,7 +4412,7 @@ function resolvePreviewGsapBaseUrl(adapter) {
4412
4412
  }
4413
4413
  function registerPreviewRoutes(api, adapter) {
4414
4414
  const gsapBaseUrl = resolvePreviewGsapBaseUrl(adapter);
4415
- const gsapEtagSalt = `:gsap:${createHash2("sha1").update(gsapBaseUrl).digest("hex").slice(0, 12)}`;
4415
+ const gsapEtagSalt = `:gsap:${createHash3("sha1").update(gsapBaseUrl).digest("hex").slice(0, 12)}`;
4416
4416
  const previewCacheHeaders = (etag) => ({
4417
4417
  "Cache-Control": "private, no-cache",
4418
4418
  ETag: etag
@@ -4547,7 +4547,7 @@ ${etag}`;
4547
4547
  if (!compFile || !existsSync5(compFile) || !statSync4(compFile).isFile()) {
4548
4548
  return c.text("not found", 404);
4549
4549
  }
4550
- const compPathHash = createHash2("sha1").update(compPath).digest("hex");
4550
+ const compPathHash = createHash3("sha1").update(compPath).digest("hex");
4551
4551
  const capture = c.req.query(PREVIEW_CAPTURE_PARAM) !== void 0;
4552
4552
  const etag = `"comp:v2:${compPathHash}:${signature}${gsapEtagSalt}${variablesEtagSalt(vars.raw)}${capture ? ":capture" : ""}"`;
4553
4553
  const ifNoneMatch = c.req.header("If-None-Match");
@@ -5052,7 +5052,7 @@ function registerRenderRoutes(api, adapter) {
5052
5052
  // src/routes/imageThumbnail.ts
5053
5053
  import { HTTPException } from "hono/http-exception";
5054
5054
  import { open, stat } from "fs/promises";
5055
- import { createHash as createHash3 } from "crypto";
5055
+ import { createHash as createHash4 } from "crypto";
5056
5056
 
5057
5057
  // src/routes/thumbnailGenerationCoordinator.ts
5058
5058
  var ThumbnailGenerationCoordinator = class {
@@ -5230,7 +5230,7 @@ function registerImageThumbnailRoutes(api, adapter) {
5230
5230
  api.get("/projects/:id/image-thumbnail/*", async (c) => {
5231
5231
  try {
5232
5232
  const { path, info } = await resolveSource(adapter, c.req.param("id"), c.req.url);
5233
- const key = createHash3("sha256").update(JSON.stringify([path, info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs])).digest("hex");
5233
+ const key = createHash4("sha256").update(JSON.stringify([path, info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs])).digest("hex");
5234
5234
  const etag = `"${key}"`;
5235
5235
  c.header("ETag", etag);
5236
5236
  c.header("Cache-Control", "private, no-cache");
@@ -5270,7 +5270,7 @@ import {
5270
5270
  writeFileSync as writeFileSync4
5271
5271
  } from "fs";
5272
5272
  import { join as join11 } from "path";
5273
- import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
5273
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
5274
5274
  var THUMBNAIL_CACHE_VERSION = "v5";
5275
5275
  var THUMBNAIL_MAX_OUTPUT_WIDTH = 240;
5276
5276
  var THUMBNAIL_MAX_OUTPUT_HEIGHT = 135;
@@ -5344,7 +5344,7 @@ function readFileOnce(file) {
5344
5344
  function manifestKey(file) {
5345
5345
  const manifest = readFileOnce(file);
5346
5346
  if (typeof manifest === "string") return { key: "", mtimeMs: -Infinity };
5347
- const hash = createHash4("sha1").update(manifest.data).digest("hex").slice(0, 16);
5347
+ const hash = createHash5("sha1").update(manifest.data).digest("hex").slice(0, 16);
5348
5348
  return { key: `_${hash}`, mtimeMs: Math.round(manifest.mtimeMs) };
5349
5349
  }
5350
5350
  function registerThumbnailRoutes(api, adapter) {
@@ -5381,7 +5381,7 @@ function registerThumbnailRoutes(api, adapter) {
5381
5381
  let sourceKey = "";
5382
5382
  if (source !== "missing") {
5383
5383
  const html = source.data.toString("utf-8");
5384
- sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
5384
+ sourceKey = `_${createHash5("sha1").update(html).digest("hex").slice(0, 16)}`;
5385
5385
  sourceMtime = Math.round(source.mtimeMs);
5386
5386
  if (!vpWidth) {
5387
5387
  const wMatch = html.match(/data-width=["'](\d+)["']/);
@@ -5397,9 +5397,9 @@ function registerThumbnailRoutes(api, adapter) {
5397
5397
  const previewPath = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
5398
5398
  const previewUrl = `${previewPath}?${PREVIEW_CAPTURE_PARAM}=1`;
5399
5399
  const cacheDir = join11(project.dir, ".thumbnails");
5400
- const selectorKey = selector ? `_${createHash4("sha1").update(selector).digest("hex").slice(0, 16)}_${selectorIndex ?? 0}` : "";
5400
+ const selectorKey = selector ? `_${createHash5("sha1").update(selector).digest("hex").slice(0, 16)}_${selectorIndex ?? 0}` : "";
5401
5401
  const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
5402
- const inputSignatureKey = `_${createHash4("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
5402
+ const inputSignatureKey = `_${createHash5("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
5403
5403
  const outputScale = outputMode === "source" ? 1 : Math.min(1, THUMBNAIL_MAX_OUTPUT_WIDTH / compW, THUMBNAIL_MAX_OUTPUT_HEIGHT / compH);
5404
5404
  const outputWidth = Math.max(1, Math.round(compW * outputScale));
5405
5405
  const outputHeight = Math.max(1, Math.round(compH * outputScale));
@@ -5523,7 +5523,7 @@ import {
5523
5523
  } from "@hyperframes/core/atomic-file";
5524
5524
 
5525
5525
  // src/helpers/freezeFrame.ts
5526
- import { createHash as createHash5, randomBytes } from "crypto";
5526
+ import { createHash as createHash6, randomBytes } from "crypto";
5527
5527
  import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
5528
5528
  import { readMediaOffsetSeconds } from "@hyperframes/parsers/media-duration";
5529
5529
  import { resolveRateSpec, sourceTimeAt } from "@hyperframes/core/speed-ramp";
@@ -5565,7 +5565,7 @@ function freezeExtractArgs(src, mediaTime, output) {
5565
5565
  var randomStillToken = () => randomBytes(4).toString("hex");
5566
5566
  function freezeStillFileName(clipId, playhead, token = randomStillToken()) {
5567
5567
  const stem = clipId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 48) || "clip";
5568
- const idHash = createHash5("sha256").update(clipId).digest("hex").slice(0, 10);
5568
+ const idHash = createHash6("sha256").update(clipId).digest("hex").slice(0, 10);
5569
5569
  return `${stem}-${idHash}-${Math.round(playhead * 1e3)}-${token}.png`;
5570
5570
  }
5571
5571
  function readFreezeSource(html, target, playhead) {
@@ -6870,13 +6870,13 @@ function numericAttribute(element, name) {
6870
6870
  }
6871
6871
 
6872
6872
  // src/helpers/previewDocumentStore.ts
6873
- import { createHash as createHash6 } from "crypto";
6873
+ import { createHash as createHash7 } from "crypto";
6874
6874
  import { readFileSync as readFileSync14, readdirSync as readdirSync5, rmSync as rmSync6 } from "fs";
6875
6875
  import { join as join17 } from "path";
6876
6876
  import { replaceFileAtomically as replaceFileAtomically4 } from "@hyperframes/core/atomic-file";
6877
6877
  function createPreviewDocumentStore(projectDir, salt) {
6878
6878
  const dir = join17(projectDir, ".hyperframes", "preview");
6879
- const fileFor = (key) => join17(dir, `${createHash6("sha256").update(`${salt}
6879
+ const fileFor = (key) => join17(dir, `${createHash7("sha256").update(`${salt}
6880
6880
  ${key}`).digest("hex").slice(0, 32)}.html`);
6881
6881
  return {
6882
6882
  read(key) {
@@ -6997,6 +6997,7 @@ export {
6997
6997
  createBackgroundRemovalJob,
6998
6998
  createPreviewDocumentStore,
6999
6999
  createProjectSignature,
7000
+ createProjectSignatureAsync,
7000
7001
  createStudioApi,
7001
7002
  createStudioManualEditsRenderBodyScript,
7002
7003
  createStudioMotionRenderBodyScript,
@@ -7013,6 +7014,7 @@ export {
7013
7014
  historyCache,
7014
7015
  identifyFileWrite,
7015
7016
  isSafePath,
7017
+ isUncopiedMedia,
7016
7018
  listProjectHistories,
7017
7019
  onRestoredInPlace,
7018
7020
  openProjectHistory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio-server",
3
- "version": "0.8.221",
3
+ "version": "0.8.222",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/heygen-com/hyperframes",
@@ -84,8 +84,8 @@
84
84
  "linkedom": "^0.18.12",
85
85
  "postcss": "^8.5.8",
86
86
  "postcss-selector-parser": "^7.1.2",
87
- "@hyperframes/parsers": "0.8.221",
88
- "@hyperframes/core": "0.8.221"
87
+ "@hyperframes/parsers": "0.8.222",
88
+ "@hyperframes/core": "0.8.222"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@types/node": "^25.0.10",