@hyperframes/studio-server 0.8.57 → 0.8.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,14 +3,17 @@ import {
3
3
  findUnsafeMutationValues
4
4
  } from "./chunk-X62ASOGO.js";
5
5
  import {
6
+ dedupeClonedCompositionId,
7
+ findTargetElement,
6
8
  isHTMLElement,
9
+ parseSourceDocument,
7
10
  patchElementInHtml,
8
11
  probeElementInSource,
9
12
  removeElementFromHtml,
10
13
  splitElementInHtml,
11
14
  unwrapElementsFromHtml,
12
15
  wrapElementsInHtml
13
- } from "./chunk-MMVFN2BI.js";
16
+ } from "./chunk-KFPWE4Q7.js";
14
17
  import {
15
18
  getElementScreenshotClip
16
19
  } from "./chunk-W2SBTCO2.js";
@@ -19,12 +22,12 @@ import {
19
22
  isAutoProxyEnabled,
20
23
  proxyEtagSalt,
21
24
  resolvePreviewMediaCodecProbeCache
22
- } from "./chunk-QN7UPP3B.js";
25
+ } from "./chunk-PGL4S2ZU.js";
23
26
  import {
24
27
  ProxyCapacityError,
25
28
  ProxyTranscodeError,
26
29
  resolveProxy
27
- } from "./chunk-BVSPLTYT.js";
30
+ } from "./chunk-45POWYFS.js";
28
31
  import {
29
32
  PROXY_VARIANT_CONFIG,
30
33
  decideMediaProxyEligibility,
@@ -33,7 +36,7 @@ import {
33
36
  probeMediaMetadata,
34
37
  recordProxyRequest,
35
38
  resolveProxyVariantRequest
36
- } from "./chunk-QJ73CZFQ.js";
39
+ } from "./chunk-4MNTZVVZ.js";
37
40
  import {
38
41
  STUDIO_MANUAL_EDITS_PATH,
39
42
  createStudioManualEditsRenderBodyScript,
@@ -297,8 +300,8 @@ import {
297
300
  existsSync as existsSync3,
298
301
  ftruncateSync,
299
302
  openSync,
300
- readFileSync as readFileSync4,
301
- writeFileSync as writeFileSync4,
303
+ readFileSync as readFileSync5,
304
+ writeFileSync as writeFileSync5,
302
305
  writeSync,
303
306
  mkdirSync as mkdirSync3,
304
307
  unlinkSync as unlinkSync2,
@@ -607,6 +610,13 @@ function recordFileWriteReceipt(absPath, receipt) {
607
610
  current.push({ ...receipt, recordedAt: now });
608
611
  receipts.set(absPath, current);
609
612
  }
613
+ function clearFileWriteReceipt(absPath, version, writeToken) {
614
+ const current = (receipts.get(absPath) ?? []).filter(
615
+ (entry) => entry.version !== version || entry.writeToken !== writeToken
616
+ );
617
+ if (current.length > 0) receipts.set(absPath, current);
618
+ else receipts.delete(absPath);
619
+ }
610
620
  function identifyFileWrite(absPath, expectedVersion) {
611
621
  const now = Date.now();
612
622
  const current = (receipts.get(absPath) ?? []).filter(
@@ -624,6 +634,79 @@ function identifyFileWrite(absPath, expectedVersion) {
624
634
  }
625
635
  var consumeFileWriteReceipt = identifyFileWrite;
626
636
 
637
+ // src/helpers/applyFileMutations.ts
638
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
639
+ function applyFileMutations(projectDir, mutations, requestToken, writeFile = writeFileSync4) {
640
+ const prepared = mutations.map((mutation) => ({
641
+ ...mutation,
642
+ before: mutation.before ?? readFileSync3(mutation.absPath, "utf-8")
643
+ }));
644
+ const results = [];
645
+ const attempted = [];
646
+ try {
647
+ for (const mutation of prepared) {
648
+ results.push(applyOneMutation(projectDir, mutation, requestToken, writeFile, attempted));
649
+ }
650
+ return results;
651
+ } catch (error) {
652
+ const rollbackErrors = [];
653
+ for (const mutation of attempted.reverse()) {
654
+ try {
655
+ writeFile(mutation.absPath, mutation.before, "utf-8");
656
+ clearFileWriteReceipt(mutation.absPath, mutation.version, mutation.writeToken);
657
+ } catch (rollbackError) {
658
+ rollbackErrors.push(rollbackError);
659
+ }
660
+ }
661
+ if (rollbackErrors.length > 0) {
662
+ throw new AggregateError(
663
+ [error, ...rollbackErrors],
664
+ "File mutation failed and rollback did not complete"
665
+ );
666
+ }
667
+ throw error;
668
+ }
669
+ }
670
+ function applyOneMutation(projectDir, mutation, requestToken, writeFile, attempted) {
671
+ const current = readFileSync3(mutation.absPath, "utf-8");
672
+ assertExpectedVersion(mutation.expectedVersion, current);
673
+ if (mutation.after === mutation.before) {
674
+ return {
675
+ ...mutation,
676
+ before: current,
677
+ changed: false,
678
+ backupPath: null,
679
+ version: fileContentVersion(mutation.before),
680
+ writeToken: null
681
+ };
682
+ }
683
+ const backup = snapshotBeforeWrite(projectDir, mutation.absPath);
684
+ if (backup.error) throw new Error(`backup failed: ${backup.error}`);
685
+ const before = current;
686
+ const version = fileContentVersion(mutation.after);
687
+ const writeToken = createWriteToken(requestToken);
688
+ attempted.push({ ...mutation, before, version, writeToken });
689
+ writeFile(mutation.absPath, mutation.after, "utf-8");
690
+ recordFileWriteReceipt(mutation.absPath, {
691
+ path: mutation.sourceFile,
692
+ version,
693
+ writeToken
694
+ });
695
+ return {
696
+ ...mutation,
697
+ before,
698
+ changed: true,
699
+ backupPath: backupPathForResponse(projectDir, backup.backupPath),
700
+ version,
701
+ writeToken
702
+ };
703
+ }
704
+ function assertExpectedVersion(expectedVersion, current) {
705
+ if (expectedVersion !== void 0 && fileContentVersion(current) !== expectedVersion) {
706
+ throw new Error("file changed since the timeline was read");
707
+ }
708
+ }
709
+
627
710
  // src/routes/files.ts
628
711
  import { classifyPropertyGroup } from "@hyperframes/parsers/gsap-constants";
629
712
  import { parseGsapScriptAcorn } from "@hyperframes/parsers/gsap-parser-acorn";
@@ -659,7 +742,7 @@ import {
659
742
  import { parseHTML as parseHTML2 } from "linkedom";
660
743
 
661
744
  // src/helpers/compositionInsertion.ts
662
- import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync } from "fs";
745
+ import { existsSync as existsSync2, readFileSync as readFileSync4, realpathSync } from "fs";
663
746
  import { randomUUID as randomUUID2 } from "crypto";
664
747
  import { dirname as dirname2, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
665
748
  import { parseHTML } from "linkedom";
@@ -731,7 +814,7 @@ function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
731
814
  }
732
815
  if (visited.has(file)) return;
733
816
  visiting.add(file);
734
- const source = readFileSync3(file, "utf-8");
817
+ const source = readFileSync4(file, "utf-8");
735
818
  const { document } = compositionRoot(source);
736
819
  for (const host of descendants(document, "[data-composition-src]")) {
737
820
  const dependency = host.getAttribute("data-composition-src");
@@ -795,7 +878,7 @@ function insertCompositionIntoSource(input) {
795
878
  const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
796
879
  const sourceAbs = canonicalProjectFile(input.projectDir, input.sourcePath);
797
880
  validateDependencyGraph(input.projectDir, targetAbs, sourceAbs);
798
- const source = readFileSync3(sourceAbs, "utf-8");
881
+ const source = readFileSync4(sourceAbs, "utf-8");
799
882
  const sourceComposition = compositionRoot(source).root;
800
883
  const duration = positiveAttribute(
801
884
  sourceComposition,
@@ -983,7 +1066,7 @@ function foldElementPatches(originalContent, patches) {
983
1066
  }
984
1067
  return { content, matched };
985
1068
  }
986
- function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSync4) {
1069
+ function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSync5, requestToken) {
987
1070
  const resolvedPaths = /* @__PURE__ */ new Set();
988
1071
  const prepared = [];
989
1072
  for (const batch of batches) {
@@ -993,7 +1076,7 @@ function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSyn
993
1076
  resolvedPaths.add(absPath);
994
1077
  let before;
995
1078
  try {
996
- before = readFileSync4(absPath, "utf-8");
1079
+ before = readFileSync5(absPath, "utf-8");
997
1080
  } catch {
998
1081
  return { error: "not-found", sourceFile: batch.sourceFile };
999
1082
  }
@@ -1019,64 +1102,34 @@ function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSyn
1019
1102
  }))
1020
1103
  };
1021
1104
  }
1022
- const files = [];
1023
- const attemptedWrites = [];
1024
- try {
1025
- for (const file of prepared) {
1026
- if (file.after === file.before) {
1027
- files.push({
1028
- sourceFile: file.sourceFile,
1029
- changed: false,
1030
- matched: file.matched,
1031
- before: file.before,
1032
- after: file.before
1033
- });
1034
- continue;
1035
- }
1036
- const backup = snapshotBeforeWrite(projectDir, file.absPath);
1037
- if (backup.error) {
1038
- throw new Error(`Failed to create backup for ${file.sourceFile}: ${backup.error}`);
1039
- }
1040
- attemptedWrites.push(file);
1041
- writeFile(file.absPath, file.after, "utf-8");
1042
- files.push({
1043
- sourceFile: file.sourceFile,
1044
- changed: true,
1045
- matched: file.matched,
1046
- before: file.before,
1047
- after: file.after,
1048
- backupPath: backupPathForResponse(projectDir, backup.backupPath)
1049
- });
1050
- }
1051
- } catch (error) {
1052
- const rollbackErrors = [];
1053
- for (const file of attemptedWrites.reverse()) {
1054
- try {
1055
- writeFile(file.absPath, file.before, "utf-8");
1056
- } catch (rollbackError) {
1057
- rollbackErrors.push(rollbackError);
1058
- }
1059
- }
1060
- if (rollbackErrors.length > 0) {
1061
- throw new AggregateError(
1062
- [error, ...rollbackErrors],
1063
- "Element patch batch failed and rollback did not complete"
1064
- );
1065
- }
1066
- throw error;
1067
- }
1105
+ const applied = applyFileMutations(
1106
+ projectDir,
1107
+ prepared.map(({ sourceFile, absPath, before, after }) => ({
1108
+ sourceFile,
1109
+ absPath,
1110
+ before,
1111
+ after
1112
+ })),
1113
+ requestToken,
1114
+ writeFile
1115
+ );
1116
+ const files = applied.map((file, index) => ({
1117
+ sourceFile: file.sourceFile,
1118
+ changed: file.changed,
1119
+ matched: prepared[index]?.matched ?? [],
1120
+ before: file.before,
1121
+ after: file.after,
1122
+ backupPath: file.backupPath ?? void 0
1123
+ }));
1068
1124
  return { durable: true, files };
1069
1125
  }
1070
1126
  function commitElementPatchBatchesWithReceipts(c, projectDir, batches) {
1071
- const result = commitElementPatchBatches(projectDir, batches);
1072
- if ("error" in result || !result.durable) return result;
1073
- for (const file of result.files) {
1074
- if (!file.changed) continue;
1075
- const absPath = resolveWithinProject(projectDir, file.sourceFile);
1076
- if (!absPath) throw new Error(`Committed element patch escaped project: ${file.sourceFile}`);
1077
- recordMutationReceipt(c, file.sourceFile, absPath, file.after);
1078
- }
1079
- return result;
1127
+ return commitElementPatchBatches(
1128
+ projectDir,
1129
+ batches,
1130
+ writeFileSync5,
1131
+ c.req.header("X-Hyperframes-Write-Token")
1132
+ );
1080
1133
  }
1081
1134
  function recordMutationReceipt(c, filePath, absPath, html) {
1082
1135
  const version = fileContentVersion(html);
@@ -1085,7 +1138,7 @@ function recordMutationReceipt(c, filePath, absPath, html) {
1085
1138
  return { version, writeToken };
1086
1139
  }
1087
1140
  function writeFileWithReceipt(c, filePath, absPath, html) {
1088
- writeFileSync4(absPath, html, "utf-8");
1141
+ writeFileSync5(absPath, html, "utf-8");
1089
1142
  return recordMutationReceipt(c, filePath, absPath, html);
1090
1143
  }
1091
1144
  function writeMutationResult(c, projectDir, filePath, absPath, html) {
@@ -1170,11 +1223,11 @@ function updateReferences(projectDir, oldPath, newPath) {
1170
1223
  let updatedCount = 0;
1171
1224
  for (const file of textFiles) {
1172
1225
  if (!isSafePath(projectDir, file)) continue;
1173
- const content = readFileSync4(file, "utf-8");
1226
+ const content = readFileSync5(file, "utf-8");
1174
1227
  if (!content.includes(oldPath)) continue;
1175
1228
  const updated = content.split(oldPath).join(newPath);
1176
1229
  if (updated !== content) {
1177
- writeFileSync4(file, updated, "utf-8");
1230
+ writeFileSync5(file, updated, "utf-8");
1178
1231
  updatedCount++;
1179
1232
  }
1180
1233
  }
@@ -1444,7 +1497,7 @@ function containsRawGsapExpression(value) {
1444
1497
  return Object.values(value).some(containsRawGsapExpression);
1445
1498
  }
1446
1499
  async function prepareGsapMutationScript(c, res, firstMutation) {
1447
- const beforeHtml = readFileSync4(res.absPath, "utf-8");
1500
+ const beforeHtml = readFileSync5(res.absPath, "utf-8");
1448
1501
  let html = beforeHtml;
1449
1502
  let block = extractGsapScriptBlock(html);
1450
1503
  if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
@@ -1511,7 +1564,7 @@ async function applyGsapMutations(c, res, mutations) {
1511
1564
  const changed = block.scriptText !== initialScript;
1512
1565
  const newHtml = changed ? block.replaceScript(block.scriptText) : html;
1513
1566
  let backupPath = null;
1514
- if (readFileSync4(res.absPath, "utf-8") !== beforeHtml) {
1567
+ if (readFileSync5(res.absPath, "utf-8") !== beforeHtml) {
1515
1568
  return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
1516
1569
  }
1517
1570
  if (changed) {
@@ -2294,7 +2347,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2294
2347
  try {
2295
2348
  const fd = openSync(finalPath, "wx");
2296
2349
  try {
2297
- writeFileSync4(fd, buffer);
2350
+ writeFileSync5(fd, buffer);
2298
2351
  } finally {
2299
2352
  closeSync(fd);
2300
2353
  }
@@ -2332,7 +2385,7 @@ function registerFileRoutes(api, adapter) {
2332
2385
  }
2333
2386
  return c.json({ error: "not found" }, 404);
2334
2387
  }
2335
- const content = readFileSync4(res.absPath);
2388
+ const content = readFileSync5(res.absPath);
2336
2389
  const version = fileContentVersion(content);
2337
2390
  c.header("ETag", version);
2338
2391
  return c.json({ filename: res.filePath, content: content.toString("utf-8"), version });
@@ -2346,7 +2399,7 @@ function registerFileRoutes(api, adapter) {
2346
2399
  if (expectedVersion === null && !createOnly) {
2347
2400
  let currentContent = null;
2348
2401
  try {
2349
- currentContent = readFileSync4(res.absPath);
2402
+ currentContent = readFileSync5(res.absPath);
2350
2403
  } catch (error) {
2351
2404
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
2352
2405
  throw error;
@@ -2372,7 +2425,7 @@ function registerFileRoutes(api, adapter) {
2372
2425
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
2373
2426
  throw error;
2374
2427
  }
2375
- const currentContent = readFileSync4(res.absPath);
2428
+ const currentContent = readFileSync5(res.absPath);
2376
2429
  return c.json(
2377
2430
  {
2378
2431
  error: "file conflict",
@@ -2407,7 +2460,7 @@ function registerFileRoutes(api, adapter) {
2407
2460
  );
2408
2461
  }
2409
2462
  try {
2410
- const currentContent = readFileSync4(fd);
2463
+ const currentContent = readFileSync5(fd);
2411
2464
  const currentVersion = fileContentVersion(currentContent);
2412
2465
  if (expectedVersion !== currentVersion) {
2413
2466
  return c.json(
@@ -2446,7 +2499,7 @@ function registerFileRoutes(api, adapter) {
2446
2499
  ensureDir(res.absPath);
2447
2500
  const body = Buffer.from(await c.req.arrayBuffer());
2448
2501
  try {
2449
- writeFileSync4(res.absPath, body, { flag: "wx" });
2502
+ writeFileSync5(res.absPath, body, { flag: "wx" });
2450
2503
  } catch (error) {
2451
2504
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
2452
2505
  throw error;
@@ -2480,7 +2533,7 @@ function registerFileRoutes(api, adapter) {
2480
2533
  }
2481
2534
  let before;
2482
2535
  try {
2483
- before = readFileSync4(ctx.absPath, "utf-8");
2536
+ before = readFileSync5(ctx.absPath, "utf-8");
2484
2537
  } catch (error) {
2485
2538
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
2486
2539
  throw error;
@@ -2537,7 +2590,7 @@ function registerFileRoutes(api, adapter) {
2537
2590
  }
2538
2591
  const parsed = await parseMutationBody(c);
2539
2592
  if ("error" in parsed) return parsed.error;
2540
- const originalContent = readFileSync4(ctx.absPath, "utf-8");
2593
+ const originalContent = readFileSync5(ctx.absPath, "utf-8");
2541
2594
  return writeIfChanged(
2542
2595
  c,
2543
2596
  ctx.project.dir,
@@ -2558,7 +2611,7 @@ function registerFileRoutes(api, adapter) {
2558
2611
  if (!Array.isArray(targets) || targets.length === 0) {
2559
2612
  return c.json({ error: "targets required" }, 400);
2560
2613
  }
2561
- const originalContent = readFileSync4(ctx.absPath, "utf-8");
2614
+ const originalContent = readFileSync5(ctx.absPath, "utf-8");
2562
2615
  let next = originalContent;
2563
2616
  for (const target of targets) {
2564
2617
  next = removeElementFromHtml(next, target);
@@ -2591,7 +2644,7 @@ function registerFileRoutes(api, adapter) {
2591
2644
  seen.add(absPath);
2592
2645
  let before;
2593
2646
  try {
2594
- before = readFileSync4(absPath, "utf-8");
2647
+ before = readFileSync5(absPath, "utf-8");
2595
2648
  } catch {
2596
2649
  return c.json({ error: `not found: ${file.path}` }, 404);
2597
2650
  }
@@ -2618,7 +2671,7 @@ function registerFileRoutes(api, adapter) {
2618
2671
  prepared.push(folded);
2619
2672
  }
2620
2673
  for (const file of prepared) {
2621
- const current = readFileSync4(file.absPath, "utf-8");
2674
+ const current = readFileSync5(file.absPath, "utf-8");
2622
2675
  if (current !== file.before) {
2623
2676
  return c.json(
2624
2677
  {
@@ -2648,7 +2701,7 @@ function registerFileRoutes(api, adapter) {
2648
2701
  const written = [];
2649
2702
  try {
2650
2703
  for (const file of prepared) {
2651
- writeFileSync4(file.absPath, file.after, "utf-8");
2704
+ writeFileSync5(file.absPath, file.after, "utf-8");
2652
2705
  written.push(file);
2653
2706
  recordFileWriteReceipt(file.absPath, {
2654
2707
  path: file.path,
@@ -2660,12 +2713,12 @@ function registerFileRoutes(api, adapter) {
2660
2713
  const conflicts = [];
2661
2714
  for (const file of written.reverse()) {
2662
2715
  try {
2663
- const current = readFileSync4(file.absPath, "utf-8");
2716
+ const current = readFileSync5(file.absPath, "utf-8");
2664
2717
  if (current !== file.after) {
2665
2718
  conflicts.push(file.path);
2666
2719
  continue;
2667
2720
  }
2668
- writeFileSync4(file.absPath, file.before, "utf-8");
2721
+ writeFileSync5(file.absPath, file.before, "utf-8");
2669
2722
  recordFileWriteReceipt(file.absPath, {
2670
2723
  path: file.path,
2671
2724
  version: fileContentVersion(file.before),
@@ -2708,7 +2761,7 @@ function registerFileRoutes(api, adapter) {
2708
2761
  const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
2709
2762
  let originalContent;
2710
2763
  try {
2711
- originalContent = readFileSync4(ctx.absPath, "utf-8");
2764
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2712
2765
  } catch {
2713
2766
  return c.json({ error: "not found" }, 404);
2714
2767
  }
@@ -2764,7 +2817,7 @@ function registerFileRoutes(api, adapter) {
2764
2817
  }
2765
2818
  let originalContent;
2766
2819
  try {
2767
- originalContent = readFileSync4(ctx.absPath, "utf-8");
2820
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2768
2821
  } catch {
2769
2822
  return c.json({ error: "not found" }, 404);
2770
2823
  }
@@ -2865,7 +2918,7 @@ function registerFileRoutes(api, adapter) {
2865
2918
  }
2866
2919
  let originalContent;
2867
2920
  try {
2868
- originalContent = readFileSync4(ctx.absPath, "utf-8");
2921
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2869
2922
  } catch {
2870
2923
  return c.json({ error: "not found" }, 404);
2871
2924
  }
@@ -2920,7 +2973,7 @@ function registerFileRoutes(api, adapter) {
2920
2973
  ).map((entry) => ({ target: entry.target, track: entry.track }));
2921
2974
  let originalContent;
2922
2975
  try {
2923
- originalContent = readFileSync4(ctx.absPath, "utf-8");
2976
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2924
2977
  } catch {
2925
2978
  return c.json({ error: "not found" }, 404);
2926
2979
  }
@@ -2949,7 +3002,7 @@ function registerFileRoutes(api, adapter) {
2949
3002
  if ("error" in parsed) return parsed.error;
2950
3003
  let content;
2951
3004
  try {
2952
- content = readFileSync4(ctx.absPath, "utf-8");
3005
+ content = readFileSync5(ctx.absPath, "utf-8");
2953
3006
  } catch {
2954
3007
  return c.json({ exists: false });
2955
3008
  }
@@ -2992,7 +3045,7 @@ function registerFileRoutes(api, adapter) {
2992
3045
  return c.json({ error: "forbidden" }, 403);
2993
3046
  }
2994
3047
  ensureDir(destAbs);
2995
- writeFileSync4(destAbs, readFileSync4(srcAbs));
3048
+ writeFileSync5(destAbs, readFileSync5(srcAbs));
2996
3049
  return c.json({ ok: true, path: copyPath }, 201);
2997
3050
  });
2998
3051
  const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
@@ -3022,7 +3075,7 @@ function registerFileRoutes(api, adapter) {
3022
3075
  mustExist: true
3023
3076
  });
3024
3077
  if ("error" in res) return res.error;
3025
- const html = readFileSync4(res.absPath, "utf-8");
3078
+ const html = readFileSync5(res.absPath, "utf-8");
3026
3079
  const block = extractGsapScriptBlock(html);
3027
3080
  if (!block) {
3028
3081
  return c.json({
@@ -3081,17 +3134,17 @@ function registerFileRoutes(api, adapter) {
3081
3134
  if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
3082
3135
  return c.json({ error: "expected and restore contents required" }, 400);
3083
3136
  }
3084
- const current = readFileSync4(res.absPath, "utf-8");
3137
+ const current = readFileSync5(res.absPath, "utf-8");
3085
3138
  if (current !== body.expected) {
3086
3139
  return c.json({ ok: true, restored: false, conflict: true });
3087
3140
  }
3088
- writeFileSync4(res.absPath, body.restore, "utf-8");
3141
+ writeFileSync5(res.absPath, body.restore, "utf-8");
3089
3142
  return c.json({ ok: true, restored: true, conflict: false });
3090
3143
  });
3091
3144
  }
3092
3145
 
3093
3146
  // src/routes/preview.ts
3094
- import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
3147
+ import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
3095
3148
  import { Readable } from "stream";
3096
3149
  import { join as join8, resolve as resolve4 } from "path";
3097
3150
  import { createHash as createHash3 } from "crypto";
@@ -3099,7 +3152,7 @@ import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRunt
3099
3152
  import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
3100
3153
 
3101
3154
  // src/helpers/subComposition.ts
3102
- import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
3155
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
3103
3156
  import { join as join7 } from "path";
3104
3157
  import { parseHTML as parseHTML3 } from "linkedom";
3105
3158
  import {
@@ -3211,7 +3264,7 @@ function tagRootCompositionFile(bodyHtml, compPath) {
3211
3264
  function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
3212
3265
  const compFile = join7(projectDir, compPath);
3213
3266
  if (!existsSync4(compFile)) return null;
3214
- const rawComp = rawOverride ?? readFileSync5(compFile, "utf-8");
3267
+ const rawComp = rawOverride ?? readFileSync6(compFile, "utf-8");
3215
3268
  let compHeadContent = "";
3216
3269
  let rewrittenContent;
3217
3270
  let htmlAttrs = "";
@@ -3244,7 +3297,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
3244
3297
  const indexPath = join7(projectDir, "index.html");
3245
3298
  let headContent = "";
3246
3299
  if (existsSync4(indexPath)) {
3247
- const indexHtml = readFileSync5(indexPath, "utf-8");
3300
+ const indexHtml = readFileSync6(indexPath, "utf-8");
3248
3301
  const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
3249
3302
  headContent = headMatch?.[1] ?? "";
3250
3303
  }
@@ -3288,8 +3341,8 @@ import {
3288
3341
  fstatSync,
3289
3342
  ftruncateSync as ftruncateSync2,
3290
3343
  openSync as openSync2,
3291
- readFileSync as readFileSync6,
3292
- writeFileSync as writeFileSync5,
3344
+ readFileSync as readFileSync7,
3345
+ writeFileSync as writeFileSync6,
3293
3346
  writeSync as writeSync2
3294
3347
  } from "fs";
3295
3348
  function persistHfIdsIfNeeded(filePath, html) {
@@ -3298,9 +3351,9 @@ function persistHfIdsIfNeeded(filePath, html) {
3298
3351
  const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
3299
3352
  if (idsAfter > idsBefore) {
3300
3353
  try {
3301
- const current = readFileSync6(filePath, "utf-8");
3354
+ const current = readFileSync7(filePath, "utf-8");
3302
3355
  if (current === html) {
3303
- writeFileSync5(filePath, normalized, "utf-8");
3356
+ writeFileSync6(filePath, normalized, "utf-8");
3304
3357
  }
3305
3358
  } catch (err) {
3306
3359
  console.warn("[hyperframes] persistHfIdsIfNeeded: failed to write ids to disk:", err);
@@ -3326,7 +3379,7 @@ function stampFileHfIds(filePath) {
3326
3379
  if (fd === null) return null;
3327
3380
  try {
3328
3381
  if (!fstatSync(fd).isFile()) return null;
3329
- const html = readFileSync6(fd, "utf-8");
3382
+ const html = readFileSync7(fd, "utf-8");
3330
3383
  const normalized = ensureHfIds(html);
3331
3384
  const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
3332
3385
  const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
@@ -3388,7 +3441,7 @@ function readStudioMotionManifestContent(projectDir) {
3388
3441
  const manifestPath = join8(projectDir, STUDIO_MOTION_PATH);
3389
3442
  if (!existsSync5(manifestPath)) return "";
3390
3443
  try {
3391
- return readFileSync7(manifestPath, "utf-8");
3444
+ return readFileSync8(manifestPath, "utf-8");
3392
3445
  } catch {
3393
3446
  return "";
3394
3447
  }
@@ -3534,14 +3587,14 @@ function resolveProjectMainHtml(projectDir, projectId) {
3534
3587
  const indexPath = join8(projectDir, "index.html");
3535
3588
  if (existsSync5(indexPath)) {
3536
3589
  return {
3537
- html: readFileSync7(indexPath, "utf-8"),
3590
+ html: readFileSync8(indexPath, "utf-8"),
3538
3591
  compositionPath: "index.html"
3539
3592
  };
3540
3593
  }
3541
3594
  const blockHtmlPath = join8(projectDir, `${projectId}.html`);
3542
3595
  if (existsSync5(blockHtmlPath)) {
3543
3596
  return {
3544
- html: readFileSync7(blockHtmlPath, "utf-8"),
3597
+ html: readFileSync8(blockHtmlPath, "utf-8"),
3545
3598
  compositionPath: `${projectId}.html`
3546
3599
  };
3547
3600
  }
@@ -3735,7 +3788,7 @@ ${runtimeTag}`;
3735
3788
  }
3736
3789
  servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
3737
3790
  }
3738
- const textBuffer = isText ? Buffer.from(readFileSync7(file, "utf-8"), "utf-8") : null;
3791
+ const textBuffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : null;
3739
3792
  const totalSize = textBuffer ? textBuffer.length : statSync3(servedPath).size;
3740
3793
  const bodyFor = (start, end) => textBuffer ? new Uint8Array(textBuffer.subarray(start, end + 1)) : (
3741
3794
  // Node's web stream type and the DOM one do not overlap for tsc on
@@ -3779,7 +3832,7 @@ ${runtimeTag}`;
3779
3832
  }
3780
3833
 
3781
3834
  // src/routes/lint.ts
3782
- import { readFileSync as readFileSync8 } from "fs";
3835
+ import { readFileSync as readFileSync9 } from "fs";
3783
3836
  import { join as join9 } from "path";
3784
3837
  async function collectProjectFindings(adapter, projectDir) {
3785
3838
  const findings = [];
@@ -3798,7 +3851,7 @@ async function lintUncoveredHtml(adapter, projectDir, htmlFiles, coveredFiles) {
3798
3851
  const findings = [];
3799
3852
  for (const file of htmlFiles) {
3800
3853
  if (coveredFiles.has(file)) continue;
3801
- const content = readFileSync8(join9(projectDir, file), "utf-8");
3854
+ const content = readFileSync9(join9(projectDir, file), "utf-8");
3802
3855
  const result = await adapter.lint(content, { filePath: file, isSubComposition: true });
3803
3856
  for (const finding of result?.findings ?? []) {
3804
3857
  findings.push({ ...finding, file });
@@ -3831,7 +3884,7 @@ function registerLintRoutes(api, adapter) {
3831
3884
 
3832
3885
  // src/routes/render.ts
3833
3886
  import { streamSSE } from "hono/streaming";
3834
- import { existsSync as existsSync6, readFileSync as readFileSync9, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
3887
+ import { existsSync as existsSync6, readFileSync as readFileSync10, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
3835
3888
  import { join as join10 } from "path";
3836
3889
  import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
3837
3890
  import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
@@ -3969,7 +4022,7 @@ function registerRenderRoutes(api, adapter) {
3969
4022
  }
3970
4023
  const contentType = renderContentType(job.outputPath);
3971
4024
  const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
3972
- const content = readFileSync9(job.outputPath);
4025
+ const content = readFileSync10(job.outputPath);
3973
4026
  return new Response(content, {
3974
4027
  headers: {
3975
4028
  "Content-Type": contentType,
@@ -3987,7 +4040,7 @@ function registerRenderRoutes(api, adapter) {
3987
4040
  }
3988
4041
  const contentType = renderContentType(job.outputPath);
3989
4042
  const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
3990
- const content = readFileSync9(job.outputPath);
4043
+ const content = readFileSync10(job.outputPath);
3991
4044
  return new Response(content, {
3992
4045
  headers: {
3993
4046
  "Content-Type": contentType,
@@ -4020,7 +4073,7 @@ function registerRenderRoutes(api, adapter) {
4020
4073
  if (!fp) return c.json({ error: "forbidden" }, 403);
4021
4074
  if (!existsSync6(fp)) return c.json({ error: "not found" }, 404);
4022
4075
  const contentType = renderContentType(fp);
4023
- const content = readFileSync9(fp);
4076
+ const content = readFileSync10(fp);
4024
4077
  return new Response(content, {
4025
4078
  headers: {
4026
4079
  "Content-Type": contentType,
@@ -4044,7 +4097,7 @@ function registerRenderRoutes(api, adapter) {
4044
4097
  let durationMs;
4045
4098
  if (existsSync6(metaPath)) {
4046
4099
  try {
4047
- const meta = JSON.parse(readFileSync9(metaPath, "utf-8"));
4100
+ const meta = JSON.parse(readFileSync10(metaPath, "utf-8"));
4048
4101
  if (meta.status === "failed" && !existsSync6(fp)) status = "failed";
4049
4102
  if (meta.durationMs) durationMs = meta.durationMs;
4050
4103
  } catch {
@@ -4078,13 +4131,13 @@ function registerRenderRoutes(api, adapter) {
4078
4131
  import {
4079
4132
  existsSync as existsSync7,
4080
4133
  mkdirSync as mkdirSync5,
4081
- readFileSync as readFileSync10,
4134
+ readFileSync as readFileSync11,
4082
4135
  readdirSync as readdirSync6,
4083
4136
  renameSync as renameSync3,
4084
4137
  rmSync as rmSync4,
4085
4138
  statSync as statSync5,
4086
4139
  unlinkSync as unlinkSync4,
4087
- writeFileSync as writeFileSync6
4140
+ writeFileSync as writeFileSync7
4088
4141
  } from "fs";
4089
4142
  import { join as join11 } from "path";
4090
4143
  import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
@@ -4233,7 +4286,7 @@ function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
4233
4286
  function writeThumbnailAtomically(path, buffer) {
4234
4287
  const temporaryPath = `${path}.${process.pid}.${randomUUID3()}.tmp`;
4235
4288
  try {
4236
- writeFileSync6(temporaryPath, buffer, { flag: "wx" });
4289
+ writeFileSync7(temporaryPath, buffer, { flag: "wx" });
4237
4290
  renameSync3(temporaryPath, path);
4238
4291
  } finally {
4239
4292
  rmSync4(temporaryPath, { force: true });
@@ -4271,7 +4324,7 @@ function registerThumbnailRoutes(api, adapter) {
4271
4324
  let sourceKey = "";
4272
4325
  const htmlFile = join11(project.dir, compPath);
4273
4326
  if (existsSync7(htmlFile)) {
4274
- const html = readFileSync10(htmlFile, "utf-8");
4327
+ const html = readFileSync11(htmlFile, "utf-8");
4275
4328
  sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
4276
4329
  sourceMtime = Math.round(statSync5(htmlFile).mtimeMs);
4277
4330
  if (!vpWidth) {
@@ -4284,14 +4337,14 @@ function registerThumbnailRoutes(api, adapter) {
4284
4337
  const manualEditsFile = join11(project.dir, STUDIO_MANUAL_EDITS_PATH);
4285
4338
  let manualEditsKey = "";
4286
4339
  if (existsSync7(manualEditsFile)) {
4287
- const manualEditsContent = readFileSync10(manualEditsFile, "utf-8");
4340
+ const manualEditsContent = readFileSync11(manualEditsFile, "utf-8");
4288
4341
  manualEditsKey = `_${createHash4("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
4289
4342
  sourceMtime = Math.max(sourceMtime, Math.round(statSync5(manualEditsFile).mtimeMs));
4290
4343
  }
4291
4344
  const motionFile = join11(project.dir, STUDIO_MOTION_PATH);
4292
4345
  let motionKey = "";
4293
4346
  if (existsSync7(motionFile)) {
4294
- const motionContent = readFileSync10(motionFile, "utf-8");
4347
+ const motionContent = readFileSync11(motionFile, "utf-8");
4295
4348
  motionKey = `_${createHash4("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
4296
4349
  sourceMtime = Math.max(sourceMtime, Math.round(statSync5(motionFile).mtimeMs));
4297
4350
  }
@@ -4313,7 +4366,7 @@ function registerThumbnailRoutes(api, adapter) {
4313
4366
  );
4314
4367
  }
4315
4368
  if (existsSync7(cachePath)) {
4316
- return new Response(new Uint8Array(readFileSync10(cachePath)), {
4369
+ return new Response(new Uint8Array(readFileSync11(cachePath)), {
4317
4370
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
4318
4371
  });
4319
4372
  }
@@ -4368,7 +4421,7 @@ function registerThumbnailRoutes(api, adapter) {
4368
4421
  }
4369
4422
 
4370
4423
  // src/routes/waveform.ts
4371
- import { existsSync as existsSync8, readFileSync as readFileSync11, statSync as statSync6 } from "fs";
4424
+ import { existsSync as existsSync8, readFileSync as readFileSync12, statSync as statSync6 } from "fs";
4372
4425
  import { join as join12 } from "path";
4373
4426
  function registerWaveformRoutes(api, adapter) {
4374
4427
  api.get("/projects/:id/waveform/*", async (c) => {
@@ -4384,7 +4437,7 @@ function registerWaveformRoutes(api, adapter) {
4384
4437
  const cachePath = join12(cacheDir, buildWaveformCacheKey(assetPath, stats));
4385
4438
  try {
4386
4439
  if (isWaveformCacheDirectory(cacheDir) && existsSync8(cachePath)) {
4387
- const peaks2 = JSON.parse(readFileSync11(cachePath, "utf-8"));
4440
+ const peaks2 = JSON.parse(readFileSync12(cachePath, "utf-8"));
4388
4441
  return c.json({ peaks: peaks2 });
4389
4442
  }
4390
4443
  } catch {
@@ -4870,14 +4923,14 @@ function registerMediaRoutes(api, adapter, options = {}) {
4870
4923
  }
4871
4924
 
4872
4925
  // src/routes/globalAssets.ts
4873
- import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
4926
+ import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
4874
4927
  import { homedir } from "os";
4875
4928
  import { join as join14 } from "path";
4876
4929
  function readGlobalAssets(home = homedir()) {
4877
4930
  const manifestPath = join14(home, ".media", "manifest.jsonl");
4878
4931
  if (!existsSync10(manifestPath)) return [];
4879
4932
  const out = [];
4880
- for (const line of readFileSync12(manifestPath, "utf8").split("\n")) {
4933
+ for (const line of readFileSync13(manifestPath, "utf8").split("\n")) {
4881
4934
  if (!line.trim()) continue;
4882
4935
  try {
4883
4936
  const rec = JSON.parse(line);
@@ -4912,6 +4965,57 @@ function createStudioApi(adapter) {
4912
4965
  return api;
4913
4966
  }
4914
4967
 
4968
+ // src/helpers/duplicateElement.ts
4969
+ import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
4970
+ function duplicateElementInHtml(source, target, newId, at) {
4971
+ const { document, wrappedFragment } = parseSourceDocument(source);
4972
+ const element = findTargetElement(document, target);
4973
+ if (!element || !element.parentElement) return { html: source, matched: false, newId: null };
4974
+ const duration = numericAttribute(element, "data-duration");
4975
+ const track = numericAttribute(element, "data-track-index") ?? 0;
4976
+ if (duration === null || numericAttribute(element, "data-start") === null) {
4977
+ return { html: source, matched: false, newId: null };
4978
+ }
4979
+ const uniqueId = nextUniqueId(document, newId);
4980
+ rippleElements(document, element, at, duration, track);
4981
+ const clone = element.cloneNode(true);
4982
+ if (!isHTMLElement(clone)) return { html: source, matched: false, newId: null };
4983
+ clone.setAttribute("id", uniqueId);
4984
+ dedupeClonedCompositionId(document, clone);
4985
+ stripStableIds(clone);
4986
+ clone.setAttribute("data-start", String(at));
4987
+ element.parentElement.insertBefore(clone, element.nextSibling);
4988
+ const html = wrappedFragment ? document.body.innerHTML || "" : document.toString();
4989
+ return { html: ensureHfIds3(html), matched: true, newId: uniqueId };
4990
+ }
4991
+ function nextUniqueId(document, newId) {
4992
+ let uniqueId = newId;
4993
+ let suffix = 2;
4994
+ while (document.getElementById(uniqueId)) uniqueId = `${newId}-${suffix++}`;
4995
+ return uniqueId;
4996
+ }
4997
+ function rippleElements(document, element, at, duration, track) {
4998
+ for (const candidate of Array.from(document.querySelectorAll("[data-start][data-duration]"))) {
4999
+ if (candidate === element || candidate.getAttribute("data-track-index") !== String(track)) {
5000
+ continue;
5001
+ }
5002
+ const start = numericAttribute(candidate, "data-start");
5003
+ if (start !== null && start >= at) {
5004
+ candidate.setAttribute("data-start", String(start + duration));
5005
+ }
5006
+ }
5007
+ }
5008
+ function stripStableIds(element) {
5009
+ element.removeAttribute("data-hf-id");
5010
+ for (const child of Array.from(element.querySelectorAll("[data-hf-id]"))) {
5011
+ child.removeAttribute("data-hf-id");
5012
+ }
5013
+ }
5014
+ function numericAttribute(element, name) {
5015
+ const value = Number(element.getAttribute(name));
5016
+ return Number.isFinite(value) ? value : null;
5017
+ }
5018
+
4915
5019
  // src/helpers/thumbnailOutput.ts
4916
5020
  function thumbnailDeviceScaleFactor({
4917
5021
  width,
@@ -4984,6 +5088,7 @@ export {
4984
5088
  STUDIO_MANUAL_EDITS_PATH,
4985
5089
  STUDIO_MOTION_PATH,
4986
5090
  affectsProjectSignature,
5091
+ applyFileMutations,
4987
5092
  buildSubCompositionHtml,
4988
5093
  consumeFileWriteReceipt,
4989
5094
  createBackgroundRemovalJob,
@@ -4992,11 +5097,18 @@ export {
4992
5097
  createStudioManualEditsRenderBodyScript,
4993
5098
  createStudioMotionRenderBodyScript,
4994
5099
  createStudioPositionSeekReapplyScript,
5100
+ dedupeClonedCompositionId,
5101
+ duplicateElementInHtml,
4995
5102
  fileContentVersion,
5103
+ findTargetElement,
4996
5104
  getElementScreenshotClip,
4997
5105
  getMimeType,
4998
5106
  identifyFileWrite,
4999
5107
  isSafePath,
5108
+ parseSourceDocument,
5109
+ patchElementInHtml,
5110
+ removeElementFromHtml,
5111
+ splitElementInHtml,
5000
5112
  thumbnailDeviceScaleFactor,
5001
5113
  walkDir
5002
5114
  };