@hyperframes/studio-server 0.8.133 → 0.8.134

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
@@ -159,10 +159,10 @@ function registerProjectRoutes(api, adapter) {
159
159
  import { bodyLimit } from "hono/body-limit";
160
160
  import {
161
161
  closeSync,
162
- existsSync as existsSync3,
162
+ existsSync as existsSync4,
163
163
  lstatSync as lstatSync2,
164
164
  openSync,
165
- readFileSync as readFileSync3,
165
+ readFileSync as readFileSync5,
166
166
  readlinkSync,
167
167
  unlinkSync as unlinkSync2,
168
168
  rmSync as rmSync3,
@@ -172,7 +172,7 @@ import {
172
172
  readdirSync as readdirSync2,
173
173
  realpathSync
174
174
  } from "fs";
175
- import { resolve as resolve2, dirname as dirname3, join as join5 } from "path";
175
+ import { resolve as resolve2, dirname as dirname3, join as join6 } from "path";
176
176
 
177
177
  // src/helpers/mime.ts
178
178
  var MIME_TYPES = {
@@ -216,8 +216,504 @@ function getMimeType(path) {
216
216
  const ext = path.slice(path.lastIndexOf(".")).toLowerCase();
217
217
  return MIME_TYPES[ext] || "application/octet-stream";
218
218
  }
219
- function isAudioFile(name) {
220
- return (getMimeType(name) ?? "").startsWith("audio/");
219
+ function isAudioFile(name) {
220
+ return (getMimeType(name) ?? "").startsWith("audio/");
221
+ }
222
+
223
+ // src/helpers/compositionInputs.ts
224
+ import { readFileSync as readFileSync2 } from "fs";
225
+ import { createHash } from "crypto";
226
+ import { parseHTML as parseHTML3 } from "linkedom";
227
+
228
+ // src/helpers/subComposition.ts
229
+ import { existsSync, readFileSync } from "fs";
230
+ import { join as join2 } from "path";
231
+ import { parseHTML } from "linkedom";
232
+ import {
233
+ rewriteAssetPaths,
234
+ rewriteCssAssetUrls,
235
+ rewriteInlineStyleAssetUrls
236
+ } from "@hyperframes/core";
237
+ import {
238
+ deferScriptsUntilFonts,
239
+ RUNTIME_BOOTSTRAP_ATTR,
240
+ stripEmbeddedRuntimeScripts
241
+ } from "@hyperframes/core/compiler";
242
+ import { isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
243
+ import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
244
+ function rewriteRelativePaths(root, compPath, projectDir) {
245
+ const assetExists = (path) => existsSync(join2(projectDir, path));
246
+ rewriteAssetPaths(
247
+ root.querySelectorAll("[src], [href]"),
248
+ compPath,
249
+ (el, attr) => el.getAttribute(attr),
250
+ (el, attr, value) => el.setAttribute(attr, value),
251
+ assetExists
252
+ );
253
+ rewriteInlineStyleAssetUrls(
254
+ root.querySelectorAll("[style]"),
255
+ compPath,
256
+ (el) => el.getAttribute("style"),
257
+ (el, value) => el.setAttribute("style", value),
258
+ assetExists
259
+ );
260
+ for (const styleEl of root.querySelectorAll("style")) {
261
+ styleEl.textContent = rewriteCssAssetUrls(styleEl.textContent || "", compPath, assetExists);
262
+ }
263
+ }
264
+ function escapeLeadingDigitIdent(id) {
265
+ return `\\${id.charCodeAt(0).toString(16)} ${id.slice(1)}`;
266
+ }
267
+ var REGEXP_SPECIALS = /[.*+?^${}()|[\]\\]/g;
268
+ function fixDigitLeadingIdSelectors(root) {
269
+ const digitIds = /* @__PURE__ */ new Set();
270
+ for (const el of root.querySelectorAll("[id]")) {
271
+ const id = el.getAttribute("id");
272
+ if (id && /^\d/.test(id)) digitIds.add(id);
273
+ }
274
+ if (digitIds.size === 0) return;
275
+ for (const styleEl of root.querySelectorAll("style")) {
276
+ let css = styleEl.textContent || "";
277
+ for (const id of digitIds) {
278
+ const pattern = new RegExp(`#${id.replace(REGEXP_SPECIALS, "\\$&")}(?![\\w-])`, "g");
279
+ css = css.replace(pattern, `#${escapeLeadingDigitIdent(id)}`);
280
+ }
281
+ styleEl.textContent = css;
282
+ }
283
+ }
284
+ function extractFullDocumentParts(rawHtml, compPath, projectDir) {
285
+ const { document: doc } = parseHTML(rawHtml);
286
+ const rewriteTargets = [doc.head, doc.body].filter(Boolean);
287
+ for (const target of rewriteTargets) {
288
+ rewriteRelativePaths(target, compPath, projectDir);
289
+ }
290
+ fixDigitLeadingIdSelectors(doc);
291
+ const headContent = doc.head?.innerHTML ?? "";
292
+ const bodyContent = doc.body?.innerHTML ?? "";
293
+ const htmlEl = doc.documentElement;
294
+ const htmlAttrs = extractElementAttrs(htmlEl);
295
+ const bodyAttrs = doc.body ? extractElementAttrs(doc.body) : "";
296
+ return { headContent, bodyContent, htmlAttrs, bodyAttrs };
297
+ }
298
+ function extractTemplateInnerHtml(rawComp) {
299
+ const { document: doc } = parseHTML(rawComp);
300
+ const template = doc.querySelector("template");
301
+ return template ? template.innerHTML : null;
302
+ }
303
+ function escapeAttrValue(value) {
304
+ return value.replace(/&/g, "&").replace(/"/g, """);
305
+ }
306
+ function extractElementAttrs(el) {
307
+ const parts = [];
308
+ for (let i = 0; i < el.attributes.length; i++) {
309
+ const attr = el.attributes[i];
310
+ if (attr.value === "") {
311
+ parts.push(attr.name);
312
+ } else {
313
+ parts.push(`${attr.name}="${escapeAttrValue(attr.value)}"`);
314
+ }
315
+ }
316
+ return parts.join(" ");
317
+ }
318
+ var NON_RENDERED_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "LINK", "META", "TEMPLATE", "NOSCRIPT"]);
319
+ function promoteTemplateCompositionId(rawComp, body) {
320
+ let templateCompositionId;
321
+ for (const tag of rawComp.matchAll(/<template\b[^>]*/gi)) {
322
+ const id = /\bdata-composition-id\s*=\s*["']([^"']+)["']/i.exec(tag[0] ?? "")?.[1];
323
+ if (id) {
324
+ templateCompositionId = id;
325
+ break;
326
+ }
327
+ }
328
+ if (!templateCompositionId) return;
329
+ if (body.querySelector("[data-composition-id]")) return;
330
+ const root = Array.from(body.children).find((el) => !NON_RENDERED_TAGS.has(el.tagName));
331
+ root?.setAttribute("data-composition-id", templateCompositionId);
332
+ }
333
+ function tagRootCompositionFile(bodyHtml, compPath) {
334
+ const match = bodyHtml.match(/<[a-zA-Z][^>]*\bdata-composition-id=/);
335
+ if (match?.index == null) return bodyHtml;
336
+ const tagEnd = bodyHtml.indexOf(">", match.index);
337
+ if (tagEnd === -1) return bodyHtml;
338
+ if (bodyHtml.slice(match.index, tagEnd).includes("data-composition-file")) return bodyHtml;
339
+ return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
340
+ }
341
+ function rootHeadContent(rootHtml) {
342
+ return rootHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i)?.[1] ?? "";
343
+ }
344
+ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
345
+ const compFile = join2(projectDir, compPath);
346
+ if (!existsSync(compFile)) return null;
347
+ const rawComp = rawOverride ?? readFileSync(compFile, "utf-8");
348
+ let compHeadContent = "";
349
+ let rewrittenContent;
350
+ let htmlAttrs = "";
351
+ let bodyAttrs = "";
352
+ const templateInner = extractTemplateInnerHtml(rawComp);
353
+ if (templateInner != null) {
354
+ const { document: contentDoc } = parseHTML(
355
+ `<!DOCTYPE html><html><head></head><body>${templateInner}</body></html>`
356
+ );
357
+ rewriteRelativePaths(contentDoc, compPath, projectDir);
358
+ fixDigitLeadingIdSelectors(contentDoc);
359
+ promoteTemplateCompositionId(rawComp, contentDoc.body);
360
+ rewrittenContent = contentDoc.body.innerHTML || templateInner;
361
+ } else if (isFullHtmlDocument(rawComp)) {
362
+ const parts = extractFullDocumentParts(rawComp, compPath, projectDir);
363
+ compHeadContent = parts.headContent;
364
+ rewrittenContent = parts.bodyContent;
365
+ htmlAttrs = parts.htmlAttrs;
366
+ bodyAttrs = parts.bodyAttrs;
367
+ } else {
368
+ const { document: contentDoc } = parseHTML(
369
+ `<!DOCTYPE html><html><head></head><body>${rawComp}</body></html>`
370
+ );
371
+ rewriteRelativePaths(contentDoc, compPath, projectDir);
372
+ fixDigitLeadingIdSelectors(contentDoc);
373
+ rewrittenContent = contentDoc.body.innerHTML || rawComp;
374
+ }
375
+ rewrittenContent = stripEmbeddedRuntimeScripts(rewrittenContent);
376
+ const { document: scriptsDoc } = parseHTML(
377
+ `<!DOCTYPE html><html><head></head><body>${rewrittenContent}</body></html>`
378
+ );
379
+ deferScriptsUntilFonts(scriptsDoc);
380
+ rewrittenContent = scriptsDoc.body.innerHTML;
381
+ rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
382
+ const indexPath = join2(projectDir, "index.html");
383
+ let headContent = "";
384
+ if (existsSync(indexPath)) headContent = rootHeadContent(readFileSync(indexPath, "utf-8"));
385
+ if (baseHref && !hasBaseElement(headContent)) {
386
+ headContent = `<base href="${baseHref}">
387
+ ${headContent}`;
388
+ }
389
+ if (compHeadContent) headContent += `
390
+ ${compHeadContent}`;
391
+ headContent = stripEmbeddedRuntimeScripts(headContent);
392
+ headContent += `
393
+ <script ${RUNTIME_BOOTSTRAP_ATTR}="1" src="${runtimeUrl}"></script>`;
394
+ if (!headContent.includes("gsap")) {
395
+ headContent += `
396
+ <script src="${gsapCdnDist()}gsap.min.js"></script>`;
397
+ }
398
+ const htmlOpen = htmlAttrs ? `<html ${htmlAttrs}>` : "<html>";
399
+ const bodyOpen = bodyAttrs ? `<body ${bodyAttrs}>` : "<body>";
400
+ return `<!DOCTYPE html>
401
+ ${htmlOpen}
402
+ <head>
403
+ ${headContent}
404
+ </head>
405
+ ${bodyOpen}
406
+ <script>window.__timelines=window.__timelines||{};</script>
407
+ ${rewrittenContent}
408
+ </body>
409
+ </html>`;
410
+ }
411
+ function hasBaseElement(html) {
412
+ return parseHTML(html).document.querySelector("base") !== null;
413
+ }
414
+
415
+ // src/helpers/compositionInsertion.ts
416
+ import { existsSync as existsSync2 } from "fs";
417
+ import { readProjectFile } from "@hyperframes/parsers/asset-resolution";
418
+ import { randomUUID } from "crypto";
419
+ import { dirname, relative, resolve, sep } from "path";
420
+ import { parseHTML as parseHTML2 } from "linkedom";
421
+ import { ProjectRootMissingError } from "@hyperframes/core";
422
+ var CompositionInsertionError = class extends Error {
423
+ constructor(message, status) {
424
+ super(message);
425
+ this.status = status;
426
+ }
427
+ status;
428
+ };
429
+ function descendants(root, selector) {
430
+ const found = Array.from(root.querySelectorAll(selector));
431
+ for (const template of root.querySelectorAll("template")) {
432
+ found.push(...descendants(template, selector));
433
+ }
434
+ return [...new Set(found)];
435
+ }
436
+ function compositionRoot(source) {
437
+ const document = parseHTML2(source).document;
438
+ const root = descendants(document, "[data-composition-id]")[0];
439
+ if (!root) throw new CompositionInsertionError("Composition source has no root", 400);
440
+ return { document, root };
441
+ }
442
+ function positiveAttribute(root, ...names) {
443
+ for (const name of names) {
444
+ const value = Number.parseFloat(root.getAttribute(name) ?? "");
445
+ if (Number.isFinite(value) && value > 0) return value;
446
+ }
447
+ throw new CompositionInsertionError(`Composition source has no valid ${names[0]}`, 400);
448
+ }
449
+ function canonicalProjectPath(projectDir, candidate) {
450
+ if (!candidate) {
451
+ if (folderGone(projectDir)) throw new ProjectRootMissingError(projectDir);
452
+ throw new CompositionInsertionError("Composition source escapes the project", 400);
453
+ }
454
+ if (!existsSync2(candidate)) {
455
+ throw new CompositionInsertionError("Composition source was not found", 404);
456
+ }
457
+ const canonical = realpath(candidate);
458
+ if (!isSafePath(realpath(projectDir), canonical)) {
459
+ throw new CompositionInsertionError("Composition source escapes the project", 400);
460
+ }
461
+ return canonical;
462
+ }
463
+ function readCompositionSource(file) {
464
+ const read = readProjectFile(file);
465
+ if (read.kind === "folder") {
466
+ throw new CompositionInsertionError("Composition source is a folder, not an HTML file", 400);
467
+ }
468
+ if (read.kind === "missing") {
469
+ throw new CompositionInsertionError("Composition source was not found", 404);
470
+ }
471
+ return read.text;
472
+ }
473
+ function validateSourcePath(sourcePath) {
474
+ if (!sourcePath.trim() || sourcePath.includes("\0") || /^[a-z]+:/i.test(sourcePath)) {
475
+ throw new CompositionInsertionError("Invalid composition source path", 400);
476
+ }
477
+ }
478
+ function canonicalProjectFile(projectDir, sourcePath) {
479
+ validateSourcePath(sourcePath);
480
+ return canonicalProjectPath(projectDir, resolveWithinProject(projectDir, sourcePath));
481
+ }
482
+ function canonicalDependency(projectDir, ownerAbs, sourcePath) {
483
+ validateSourcePath(sourcePath);
484
+ return canonicalProjectPath(
485
+ projectDir,
486
+ resolveWithinProject(projectDir, relative(projectDir, resolve(dirname(ownerAbs), sourcePath)))
487
+ );
488
+ }
489
+ function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
490
+ const visited = /* @__PURE__ */ new Set();
491
+ const visiting = /* @__PURE__ */ new Set();
492
+ const visit = (file) => {
493
+ if (file === targetAbs) {
494
+ throw new CompositionInsertionError("Composition insertion would create a cycle", 400);
495
+ }
496
+ if (visiting.has(file)) {
497
+ throw new CompositionInsertionError("Composition dependency cycle detected", 400);
498
+ }
499
+ if (visited.has(file)) return;
500
+ visiting.add(file);
501
+ const source = readCompositionSource(file);
502
+ const { document } = compositionRoot(source);
503
+ for (const host of descendants(document, "[data-composition-src]")) {
504
+ const dependency = host.getAttribute("data-composition-src");
505
+ if (dependency) {
506
+ visit(canonicalDependency(projectDir, file, dependency));
507
+ }
508
+ }
509
+ visiting.delete(file);
510
+ visited.add(file);
511
+ };
512
+ visit(sourceAbs);
513
+ }
514
+ function numberAttribute(element, name, fallback = 0) {
515
+ const value = Number.parseFloat(element.getAttribute(name) ?? "");
516
+ return Number.isFinite(value) ? value : fallback;
517
+ }
518
+ function rangesOverlap(start, duration, other) {
519
+ const otherStart = numberAttribute(other, "data-start");
520
+ const otherDuration = numberAttribute(other, "data-duration");
521
+ return start < otherStart + otherDuration && otherStart < start + duration;
522
+ }
523
+ function resolveTrack(root, desiredTrack, start, duration) {
524
+ const clips = descendants(root, "[data-start][data-duration]").filter(
525
+ (element) => element !== root && element.parentElement?.closest("[data-composition-id]") === root
526
+ );
527
+ const tracks = [...new Set(clips.map((clip) => numberAttribute(clip, "data-track-index")))].sort(
528
+ (a, b) => a - b
529
+ );
530
+ const isFree = (track) => !clips.some(
531
+ (clip) => numberAttribute(clip, "data-track-index") === track && rangesOverlap(start, duration, clip)
532
+ );
533
+ if (isFree(desiredTrack)) return desiredTrack;
534
+ const row = tracks.indexOf(desiredTrack);
535
+ for (let index = row - 1; index >= 0; index--) {
536
+ const track = tracks[index];
537
+ if (track !== void 0 && isFree(track)) return track;
538
+ }
539
+ for (let index = Math.max(0, row + 1); index < tracks.length; index++) {
540
+ const track = tracks[index];
541
+ if (track !== void 0 && isFree(track)) return track;
542
+ }
543
+ return Math.max(desiredTrack, ...tracks, -1) + 1;
544
+ }
545
+ function uniqueHostId(root, base) {
546
+ const ids = /* @__PURE__ */ new Set([
547
+ ...descendants(root, "[id]").map((element) => element.id),
548
+ ...descendants(root, "[data-composition-id]").flatMap((element) => {
549
+ const id = element.getAttribute("data-composition-id");
550
+ return id ? [id] : [];
551
+ })
552
+ ]);
553
+ if (!ids.has(base)) return base;
554
+ let suffix = 2;
555
+ while (ids.has(`${base}_${suffix}`)) suffix += 1;
556
+ return `${base}_${suffix}`;
557
+ }
558
+ function relativeSourcePath(targetAbs, sourceAbs) {
559
+ return relative(dirname(targetAbs), sourceAbs).split(sep).join("/");
560
+ }
561
+ function insertCompositionIntoSource(input) {
562
+ const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
563
+ const sourceAbs = canonicalProjectFile(input.projectDir, input.sourcePath);
564
+ validateDependencyGraph(input.projectDir, targetAbs, sourceAbs);
565
+ const source = readCompositionSource(sourceAbs);
566
+ const sourceComposition = compositionRoot(source).root;
567
+ const duration = positiveAttribute(
568
+ sourceComposition,
569
+ "data-composition-duration",
570
+ "data-duration"
571
+ );
572
+ const width = positiveAttribute(sourceComposition, "data-width");
573
+ const height = positiveAttribute(sourceComposition, "data-height");
574
+ const { document, root } = compositionRoot(input.parentSource);
575
+ const parentDuration = positiveAttribute(root, "data-duration", "data-composition-duration");
576
+ const base = (sourceComposition.getAttribute("data-composition-id") ?? "composition").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "composition";
577
+ const hostId = uniqueHostId(root, base);
578
+ const track = resolveTrack(
579
+ root,
580
+ Math.max(0, Math.round(input.desiredTrack)),
581
+ input.start,
582
+ duration
583
+ );
584
+ const zIndex = Math.max(
585
+ 0,
586
+ ...descendants(root, "[style]").map((element) => {
587
+ const match = /(?:^|;)\s*z-index\s*:\s*(-?\d+)/i.exec(element.getAttribute("style") ?? "");
588
+ return match?.[1] ? Number.parseInt(match[1], 10) : 0;
589
+ })
590
+ ) + 1;
591
+ const host = document.createElement("div");
592
+ host.id = hostId;
593
+ host.className = "clip";
594
+ host.setAttribute("data-hf-id", `hf-${randomUUID()}`);
595
+ host.setAttribute("data-composition-id", hostId);
596
+ host.setAttribute("data-composition-src", relativeSourcePath(targetAbs, sourceAbs));
597
+ host.setAttribute("data-start", String(Math.round(input.start * 100) / 100));
598
+ host.setAttribute("data-duration", String(duration));
599
+ host.setAttribute("data-playback-start", "0");
600
+ host.setAttribute("data-track-index", String(track));
601
+ host.setAttribute("data-width", String(width));
602
+ host.setAttribute("data-height", String(height));
603
+ host.setAttribute(
604
+ "style",
605
+ `position: absolute; left: 0px; top: 0px; width: ${width}px; height: ${height}px; z-index: ${zIndex}`
606
+ );
607
+ root.appendChild(host);
608
+ if (input.start + duration > parentDuration) {
609
+ const name = root.hasAttribute("data-duration") ? "data-duration" : "data-composition-duration";
610
+ root.setAttribute(name, String(Math.round((input.start + duration) * 100) / 100));
611
+ }
612
+ return { html: document.toString(), hostId, track, duration };
613
+ }
614
+
615
+ // src/helpers/compositionInputs.ts
616
+ var ROOT_COMPOSITION = "index.html";
617
+ var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
618
+ function projectReader(projectDir) {
619
+ const sources = /* @__PURE__ */ new Map();
620
+ return (compPath) => {
621
+ if (!sources.has(compPath)) {
622
+ const file = resolveWithinProject(projectDir, compPath);
623
+ let source = null;
624
+ try {
625
+ source = file ? readFileSync2(file, "utf-8") : null;
626
+ } catch {
627
+ }
628
+ sources.set(compPath, source);
629
+ }
630
+ return sources.get(compPath) ?? null;
631
+ };
632
+ }
633
+ function normalizeSource(src) {
634
+ if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) return null;
635
+ const path = src.split(/[?#]/)[0]?.replace(/^\.?\//, "") ?? "";
636
+ return path || null;
637
+ }
638
+ function closureOf(read, compPath) {
639
+ const closure = /* @__PURE__ */ new Set();
640
+ const visit = (path) => {
641
+ if (closure.has(path)) return;
642
+ closure.add(path);
643
+ for (const match of (read(path) ?? "").matchAll(COMPOSITION_SRC)) {
644
+ const mounted = normalizeSource(match[2] ?? "");
645
+ if (mounted) visit(mounted);
646
+ }
647
+ };
648
+ visit(compPath);
649
+ return closure;
650
+ }
651
+ function lengthIsTimeline(projectDir, compPath) {
652
+ const read = projectReader(projectDir);
653
+ const documents = /* @__PURE__ */ new Map();
654
+ const elements = (path, selector) => {
655
+ let document = documents.get(path);
656
+ if (!document) documents.set(path, document = parseHTML3(read(path) ?? "").document);
657
+ return descendants(document, selector);
658
+ };
659
+ const comp = normalizeSource(compPath) ?? compPath;
660
+ const unsized = (path) => {
661
+ const [root] = elements(path, "[data-composition-id]");
662
+ return Boolean(root && !root.hasAttribute("data-duration"));
663
+ };
664
+ if (unsized(ROOT_COMPOSITION)) return true;
665
+ const hosts = [...closureOf(read, ROOT_COMPOSITION)].flatMap(
666
+ (path) => elements(path, "[data-composition-src]").filter(
667
+ (host) => normalizeSource(host.getAttribute("data-composition-src") ?? "") === comp
668
+ )
669
+ );
670
+ if (comp === ROOT_COMPOSITION || hosts.length === 0) return unsized(comp);
671
+ return hosts.some(
672
+ (host) => !host.hasAttribute("data-duration") && !host.hasAttribute("data-end")
673
+ );
674
+ }
675
+ var inputSignatures = /* @__PURE__ */ new Map();
676
+ var rootMemos = /* @__PURE__ */ new Map();
677
+ var sha1 = (text2) => createHash("sha1").update(text2).digest("hex");
678
+ function rootHashes(read) {
679
+ const source = read(ROOT_COMPOSITION) ?? "";
680
+ return { root: sha1(source), head: sha1(rootHeadContent(source)) };
681
+ }
682
+ function compositionInputSignature(projectDir, compPath, projectSignature) {
683
+ const key = `${projectDir}\0${compPath}`;
684
+ const known = inputSignatures.get(key);
685
+ if (known?.projectSignature === projectSignature) return known.inputSignature;
686
+ const read = projectReader(projectDir);
687
+ const comp = normalizeSource(compPath) ?? compPath;
688
+ const inputs = closureOf(read, comp).add(ROOT_COMPOSITION);
689
+ const excluded = new Set(
690
+ [...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path))
691
+ );
692
+ let inputSignature;
693
+ if (comp === ROOT_COMPOSITION) {
694
+ inputSignature = createProjectSignature(projectDir, excluded);
695
+ } else {
696
+ const hashes = rootHashes(read);
697
+ if (!rootMemos.has(projectDir)) rootMemos.set(projectDir, { ...hashes, affected: null });
698
+ inputSignature = `${createProjectSignature(projectDir, excluded.add(ROOT_COMPOSITION))}:${hashes.head}`;
699
+ }
700
+ inputSignatures.set(key, { projectSignature, inputSignature });
701
+ return inputSignature;
702
+ }
703
+ function compositionsAffectedBy(projectDir, changedPath) {
704
+ const changed = changedPath.replace(/\\/g, "/");
705
+ const read = projectReader(projectDir);
706
+ if (changed === ROOT_COMPOSITION) {
707
+ const now = rootHashes(read);
708
+ const memo = rootMemos.get(projectDir);
709
+ if (memo?.root === now.root) return memo.affected;
710
+ const affected = memo?.head === now.head ? [ROOT_COMPOSITION] : null;
711
+ rootMemos.set(projectDir, { ...now, affected });
712
+ return affected;
713
+ }
714
+ const mounted = closureOf(read, ROOT_COMPOSITION);
715
+ if (!mounted.has(changed)) return null;
716
+ return [...mounted].filter((path) => closureOf(read, path).has(changed));
221
717
  }
222
718
 
223
719
  // src/routes/files.ts
@@ -226,7 +722,7 @@ import { createFileAtomically, replaceFileAtomically as replaceFileAtomically2 }
226
722
  // src/helpers/waveform.ts
227
723
  import { spawn } from "child_process";
228
724
  import {
229
- existsSync,
725
+ existsSync as existsSync3,
230
726
  writeFileSync,
231
727
  mkdirSync,
232
728
  lstatSync,
@@ -235,7 +731,7 @@ import {
235
731
  rmSync,
236
732
  statSync
237
733
  } from "fs";
238
- import { dirname, join as join2 } from "path";
734
+ import { dirname as dirname2, join as join3 } from "path";
239
735
  import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
240
736
  var SAMPLE_RATE = 4e3;
241
737
  var PEAK_COUNT = 4e3;
@@ -302,12 +798,12 @@ function decodeAudioPeaks(audioPath) {
302
798
  });
303
799
  }
304
800
  async function generateWaveformCache(projectDir, assetPath) {
305
- const audioPath = join2(projectDir, assetPath);
306
- if (!existsSync(audioPath)) return;
801
+ const audioPath = join3(projectDir, assetPath);
802
+ if (!existsSync3(audioPath)) return;
307
803
  const stats = statSync(audioPath);
308
- const cacheDir = join2(projectDir, ".waveform-cache");
309
- const cachePath = join2(cacheDir, buildWaveformCacheKey(assetPath, stats));
310
- if (isWaveformCacheDirectory(cacheDir) && existsSync(cachePath)) return;
804
+ const cacheDir = join3(projectDir, ".waveform-cache");
805
+ const cachePath = join3(cacheDir, buildWaveformCacheKey(assetPath, stats));
806
+ if (isWaveformCacheDirectory(cacheDir) && existsSync3(cachePath)) return;
311
807
  const peaks = await decodeAudioPeaks(audioPath);
312
808
  writeWaveformCache(cachePath, peaks);
313
809
  }
@@ -315,7 +811,7 @@ function isWaveformCacheDirectory(cacheDir) {
315
811
  return lstatSync(cacheDir, { throwIfNoEntry: false })?.isDirectory() ?? false;
316
812
  }
317
813
  function writeWaveformCache(cachePath, peaks) {
318
- const cacheDir = dirname(cachePath);
814
+ const cacheDir = dirname2(cachePath);
319
815
  try {
320
816
  mkdirSync(cacheDir, { mode: 448 });
321
817
  } catch (error) {
@@ -324,9 +820,9 @@ function writeWaveformCache(cachePath, peaks) {
324
820
  if (!isWaveformCacheDirectory(cacheDir)) {
325
821
  throw new Error("Waveform cache must be a directory, not a symlink");
326
822
  }
327
- const stagingDir = mkdtempSync(join2(cacheDir, ".waveform-"));
823
+ const stagingDir = mkdtempSync(join3(cacheDir, ".waveform-"));
328
824
  try {
329
- const stagingPath = join2(stagingDir, "peaks.json");
825
+ const stagingPath = join3(stagingDir, "peaks.json");
330
826
  writeFileSync(stagingPath, JSON.stringify(peaks), { flag: "wx" });
331
827
  renameSync(stagingPath, cachePath);
332
828
  } finally {
@@ -341,7 +837,7 @@ function writeWaveformCache(cachePath, peaks) {
341
837
  import { spawnSync } from "child_process";
342
838
  import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
343
839
  import { tmpdir } from "os";
344
- import { basename, join as join3 } from "path";
840
+ import { basename, join as join4 } from "path";
345
841
  import { FFPROBE_PATH_ENV, findFfBinary as findFfBinary2 } from "@hyperframes/parsers/ff-binaries";
346
842
  var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
347
843
  var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac|flac)$/i;
@@ -392,8 +888,8 @@ function checkStreams(stdout, isVideo, isAudio) {
392
888
  }
393
889
  }
394
890
  function validateUploadedMediaBuffer(fileName, buffer, runner = defaultRunner) {
395
- const tempDir = mkdtempSync2(join3(tmpdir(), "hyperframes-upload-"));
396
- const tempPath = join3(tempDir, basename(fileName));
891
+ const tempDir = mkdtempSync2(join4(tmpdir(), "hyperframes-upload-"));
892
+ const tempPath = join4(tempDir, basename(fileName));
397
893
  try {
398
894
  writeFileSync2(tempPath, buffer);
399
895
  return validateUploadedMedia(tempPath, runner);
@@ -403,9 +899,9 @@ function validateUploadedMediaBuffer(fileName, buffer, runner = defaultRunner) {
403
899
  }
404
900
 
405
901
  // src/helpers/backupJournal.ts
406
- import { readdirSync, readFileSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
902
+ import { readdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
407
903
  import { Buffer as Buffer2 } from "buffer";
408
- import { join as join4, relative } from "path";
904
+ import { join as join5, relative as relative2 } from "path";
409
905
  var DEFAULT_KEEP_PER_FILE = 10;
410
906
  function backupKeyForPath(path) {
411
907
  return Buffer2.from(path, "utf-8").toString("base64url");
@@ -415,16 +911,16 @@ function timestampPrefix() {
415
911
  }
416
912
  function backupPathForResponse(projectDir, backupPath) {
417
913
  if (!backupPath) return null;
418
- const rel = relative(projectDir, backupPath);
914
+ const rel = relative2(projectDir, backupPath);
419
915
  if (!rel || rel.startsWith("..")) return null;
420
916
  return rel.split("\\").join("/");
421
917
  }
422
918
  function snapshotBeforeWrite(projectDir, absPath, options = {}) {
423
919
  if (!isSafePath(projectDir, absPath)) return { backupPath: null };
424
920
  try {
425
- const content = readFileSync(absPath);
426
- const relativePath = relative(projectDir, absPath);
427
- const backupDir = join4(projectDir, ".hyperframes", "backup");
921
+ const content = readFileSync3(absPath);
922
+ const relativePath = relative2(projectDir, absPath);
923
+ const backupDir = join5(projectDir, ".hyperframes", "backup");
428
924
  mkdirWithinProject(projectDir, backupDir);
429
925
  const backupKey = backupKeyForPath(relativePath);
430
926
  const backupPath = nextBackupPath(backupDir, backupKey);
@@ -441,18 +937,18 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
441
937
  }
442
938
  function nextBackupPath(backupDir, backupKey) {
443
939
  const base = `${timestampPrefix()}-${backupKey}`;
444
- let candidate = join4(backupDir, base);
940
+ let candidate = join5(backupDir, base);
445
941
  let counter = 2;
446
942
  while (true) {
447
943
  try {
448
- readFileSync(candidate);
944
+ readFileSync3(candidate);
449
945
  } catch (error) {
450
946
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
451
947
  return candidate;
452
948
  }
453
949
  throw error;
454
950
  }
455
- candidate = join4(backupDir, `${base}-${counter}`);
951
+ candidate = join5(backupDir, `${base}-${counter}`);
456
952
  counter += 1;
457
953
  }
458
954
  }
@@ -460,7 +956,7 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
460
956
  const keep = Math.max(1, Math.floor(keepPerFile));
461
957
  const suffix = `-${backupKey}`;
462
958
  const numberedSuffix = new RegExp(`-${backupKey}-\\d+$`);
463
- const matches = readdirSync(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join4(backupDir, name)).sort((a, b) => {
959
+ const matches = readdirSync(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join5(backupDir, name)).sort((a, b) => {
464
960
  return b.localeCompare(a);
465
961
  });
466
962
  for (const file of matches.slice(keep)) {
@@ -481,17 +977,17 @@ function replaceWithProjectDirMissing(c, hostHeaders) {
481
977
  }
482
978
 
483
979
  // src/helpers/applyFileMutations.ts
484
- import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
980
+ import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
485
981
  import { replaceFileAtomically } from "@hyperframes/core/atomic-file";
486
982
  function applyFileMutations(projectDir, mutations, requestToken, writeFile = (path, content) => replaceFileAtomically(path, content, statSync2(path).mode)) {
487
983
  const prepared = mutations.map((mutation) => ({
488
984
  ...mutation,
489
- before: mutation.before ?? readFileSync2(mutation.absPath, "utf-8")
985
+ before: mutation.before ?? readFileSync4(mutation.absPath, "utf-8")
490
986
  }));
491
987
  const results = [];
492
988
  const attempted = [];
493
989
  for (const mutation of prepared)
494
- assertExpectedVersion(mutation, readFileSync2(mutation.absPath, "utf-8"));
990
+ assertExpectedVersion(mutation, readFileSync4(mutation.absPath, "utf-8"));
495
991
  try {
496
992
  for (const mutation of prepared) {
497
993
  results.push(applyOneMutation(projectDir, mutation, requestToken, writeFile, attempted));
@@ -501,7 +997,7 @@ function applyFileMutations(projectDir, mutations, requestToken, writeFile = (pa
501
997
  const rollbackErrors = [];
502
998
  for (const mutation of attempted.reverse()) {
503
999
  try {
504
- if (!mutation.written || readFileSync2(mutation.absPath, "utf-8") === mutation.after)
1000
+ if (!mutation.written || readFileSync4(mutation.absPath, "utf-8") === mutation.after)
505
1001
  writeFile(mutation.absPath, mutation.before, "utf-8");
506
1002
  clearFileWriteReceipt(mutation.absPath, mutation.version, mutation.writeToken);
507
1003
  } catch (rollbackError) {
@@ -518,7 +1014,7 @@ function applyFileMutations(projectDir, mutations, requestToken, writeFile = (pa
518
1014
  }
519
1015
  }
520
1016
  function applyOneMutation(projectDir, mutation, requestToken, writeFile, attempted) {
521
- const current = readFileSync2(mutation.absPath, "utf-8");
1017
+ const current = readFileSync4(mutation.absPath, "utf-8");
522
1018
  assertExpectedVersion(mutation, current);
523
1019
  if (mutation.after === mutation.before) {
524
1020
  return {
@@ -532,7 +1028,7 @@ function applyOneMutation(projectDir, mutation, requestToken, writeFile, attempt
532
1028
  }
533
1029
  const backup = snapshotBeforeWrite(projectDir, mutation.absPath);
534
1030
  if (backup.error) throw new Error(`backup failed: ${backup.error}`);
535
- assertExpectedVersion(mutation, readFileSync2(mutation.absPath, "utf-8"));
1031
+ assertExpectedVersion(mutation, readFileSync4(mutation.absPath, "utf-8"));
536
1032
  const before = current;
537
1033
  const version = fileContentVersion(mutation.after);
538
1034
  const writeToken = createWriteToken(requestToken);
@@ -541,295 +1037,96 @@ function applyOneMutation(projectDir, mutation, requestToken, writeFile, attempt
541
1037
  writeFile(mutation.absPath, mutation.after, "utf-8");
542
1038
  attempt.written = true;
543
1039
  recordFileWriteReceipt(mutation.absPath, {
544
- path: mutation.sourceFile,
545
- version,
546
- writeToken,
547
- overwrote: before
548
- });
549
- return {
550
- ...mutation,
551
- before,
552
- changed: true,
553
- backupPath: backupPathForResponse(projectDir, backup.backupPath),
554
- version,
555
- writeToken
556
- };
557
- }
558
- var FileChangedError = class extends Error {
559
- constructor(sourceFile) {
560
- super("file changed since the timeline was read");
561
- this.sourceFile = sourceFile;
562
- }
563
- sourceFile;
564
- };
565
- function assertExpectedVersion(mutation, current) {
566
- const stale = mutation.expectedVersion === void 0 ? current !== mutation.before : fileContentVersion(current) !== mutation.expectedVersion;
567
- if (stale) throw new FileChangedError(mutation.sourceFile);
568
- }
569
-
570
- // src/routes/files.ts
571
- import { classifyPropertyGroup } from "@hyperframes/parsers/gsap-constants";
572
- import { findTimelineScript, parseGsapScriptAcorn } from "@hyperframes/parsers/gsap-parser-acorn";
573
- import { unrollComputedTimeline } from "@hyperframes/parsers";
574
- import {
575
- updateAnimationInScript,
576
- addAnimationToScript,
577
- removeAnimationFromScript,
578
- addKeyframeToScript,
579
- removeKeyframeFromScript,
580
- moveKeyframeInScript,
581
- resizeKeyframedTweenInScript,
582
- updateKeyframeInScript,
583
- convertToKeyframesFromScript,
584
- removeAllKeyframesFromScript,
585
- materializeKeyframesFromScript,
586
- unrollDynamicAnimations,
587
- setArcPathInScript,
588
- updateArcSegmentInScript,
589
- updateMotionPathPointInScript,
590
- addMotionPathPointInScript,
591
- removeMotionPathPointInScript,
592
- addMotionPathToScript,
593
- removeArcPathFromScript,
594
- addAnimationWithKeyframesToScript,
595
- replaceTweenWithKeyframesInScript,
596
- splitAnimationsInScript,
597
- splitIntoPropertyGroupsFromScript,
598
- retimeClipTweensInScript,
599
- dedupePositionWritesInScript,
600
- syncPositionHoldsBeforeKeyframes,
601
- clipQueryRoot
602
- } from "@hyperframes/parsers/gsap-writer-acorn";
603
-
604
- // src/helpers/studioFontFace.ts
605
- var FONT_STYLE_RE = /<style\b[^>]*data-hf-studio-fonts=(["'])true\1[^>]*>([\s\S]*?)<\/style>/i;
606
- var ONE_FONT_FACE_RULE = /^@font-face \{(?:[^{}<"'\\]|"(?:[^"\\<\n\r\f]|\\[^<\n\r\f])*"|'(?:[^'\\<\n\r\f]|\\[^<\n\r\f])*')*\}$/;
607
- function isStudioFontFaceCss(css) {
608
- return typeof css === "string" && ONE_FONT_FACE_RULE.test(css);
609
- }
610
- function ensureStudioFontFaceCss(html, css) {
611
- if (html.includes(css)) return html;
612
- const block = FONT_STYLE_RE.exec(html);
613
- if (block) {
614
- const next = `${(block[2] ?? "").trim()}
615
- ${css}`.trim();
616
- return html.replace(block[0], () => `<style data-hf-studio-fonts="true">
617
- ${next}
618
- </style>`);
619
- }
620
- const styleTag = `<style data-hf-studio-fonts="true">
621
- ${css}
622
- </style>`;
623
- if (/<\/head>/i.test(html)) return html.replace(/<\/head>/i, () => ` ${styleTag}
624
- </head>`);
625
- return `${styleTag}
626
- ${html}`;
627
- }
628
-
629
- // src/routes/files.ts
630
- import { parseHTML as parseHTML2 } from "linkedom";
631
- import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
632
-
633
- // src/helpers/compositionInsertion.ts
634
- import { existsSync as existsSync2 } from "fs";
635
- import { readProjectFile } from "@hyperframes/parsers/asset-resolution";
636
- import { randomUUID } from "crypto";
637
- import { dirname as dirname2, relative as relative2, resolve, sep } from "path";
638
- import { parseHTML } from "linkedom";
639
- import { ProjectRootMissingError } from "@hyperframes/core";
640
- var CompositionInsertionError = class extends Error {
641
- constructor(message, status) {
642
- super(message);
643
- this.status = status;
644
- }
645
- status;
646
- };
647
- function descendants(root, selector) {
648
- const found = Array.from(root.querySelectorAll(selector));
649
- for (const template of root.querySelectorAll("template")) {
650
- found.push(...descendants(template, selector));
651
- }
652
- return [...new Set(found)];
653
- }
654
- function compositionRoot(source) {
655
- const document = parseHTML(source).document;
656
- const root = descendants(document, "[data-composition-id]")[0];
657
- if (!root) throw new CompositionInsertionError("Composition source has no root", 400);
658
- return { document, root };
659
- }
660
- function positiveAttribute(root, ...names) {
661
- for (const name of names) {
662
- const value = Number.parseFloat(root.getAttribute(name) ?? "");
663
- if (Number.isFinite(value) && value > 0) return value;
664
- }
665
- throw new CompositionInsertionError(`Composition source has no valid ${names[0]}`, 400);
666
- }
667
- function canonicalProjectPath(projectDir, candidate) {
668
- if (!candidate) {
669
- if (folderGone(projectDir)) throw new ProjectRootMissingError(projectDir);
670
- throw new CompositionInsertionError("Composition source escapes the project", 400);
671
- }
672
- if (!existsSync2(candidate)) {
673
- throw new CompositionInsertionError("Composition source was not found", 404);
674
- }
675
- const canonical = realpath(candidate);
676
- if (!isSafePath(realpath(projectDir), canonical)) {
677
- throw new CompositionInsertionError("Composition source escapes the project", 400);
678
- }
679
- return canonical;
680
- }
681
- function readCompositionSource(file) {
682
- const read = readProjectFile(file);
683
- if (read.kind === "folder") {
684
- throw new CompositionInsertionError("Composition source is a folder, not an HTML file", 400);
685
- }
686
- if (read.kind === "missing") {
687
- throw new CompositionInsertionError("Composition source was not found", 404);
688
- }
689
- return read.text;
690
- }
691
- function validateSourcePath(sourcePath) {
692
- if (!sourcePath.trim() || sourcePath.includes("\0") || /^[a-z]+:/i.test(sourcePath)) {
693
- throw new CompositionInsertionError("Invalid composition source path", 400);
694
- }
695
- }
696
- function canonicalProjectFile(projectDir, sourcePath) {
697
- validateSourcePath(sourcePath);
698
- return canonicalProjectPath(projectDir, resolveWithinProject(projectDir, sourcePath));
699
- }
700
- function canonicalDependency(projectDir, ownerAbs, sourcePath) {
701
- validateSourcePath(sourcePath);
702
- return canonicalProjectPath(
703
- projectDir,
704
- resolveWithinProject(projectDir, relative2(projectDir, resolve(dirname2(ownerAbs), sourcePath)))
705
- );
706
- }
707
- function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
708
- const visited = /* @__PURE__ */ new Set();
709
- const visiting = /* @__PURE__ */ new Set();
710
- const visit = (file) => {
711
- if (file === targetAbs) {
712
- throw new CompositionInsertionError("Composition insertion would create a cycle", 400);
713
- }
714
- if (visiting.has(file)) {
715
- throw new CompositionInsertionError("Composition dependency cycle detected", 400);
716
- }
717
- if (visited.has(file)) return;
718
- visiting.add(file);
719
- const source = readCompositionSource(file);
720
- const { document } = compositionRoot(source);
721
- for (const host of descendants(document, "[data-composition-src]")) {
722
- const dependency = host.getAttribute("data-composition-src");
723
- if (dependency) {
724
- visit(canonicalDependency(projectDir, file, dependency));
725
- }
726
- }
727
- visiting.delete(file);
728
- visited.add(file);
729
- };
730
- visit(sourceAbs);
731
- }
732
- function numberAttribute(element, name, fallback = 0) {
733
- const value = Number.parseFloat(element.getAttribute(name) ?? "");
734
- return Number.isFinite(value) ? value : fallback;
735
- }
736
- function rangesOverlap(start, duration, other) {
737
- const otherStart = numberAttribute(other, "data-start");
738
- const otherDuration = numberAttribute(other, "data-duration");
739
- return start < otherStart + otherDuration && otherStart < start + duration;
740
- }
741
- function resolveTrack(root, desiredTrack, start, duration) {
742
- const clips = descendants(root, "[data-start][data-duration]").filter(
743
- (element) => element !== root && element.parentElement?.closest("[data-composition-id]") === root
744
- );
745
- const tracks = [...new Set(clips.map((clip) => numberAttribute(clip, "data-track-index")))].sort(
746
- (a, b) => a - b
747
- );
748
- const isFree = (track) => !clips.some(
749
- (clip) => numberAttribute(clip, "data-track-index") === track && rangesOverlap(start, duration, clip)
750
- );
751
- if (isFree(desiredTrack)) return desiredTrack;
752
- const row = tracks.indexOf(desiredTrack);
753
- for (let index = row - 1; index >= 0; index--) {
754
- const track = tracks[index];
755
- if (track !== void 0 && isFree(track)) return track;
756
- }
757
- for (let index = Math.max(0, row + 1); index < tracks.length; index++) {
758
- const track = tracks[index];
759
- if (track !== void 0 && isFree(track)) return track;
760
- }
761
- return Math.max(desiredTrack, ...tracks, -1) + 1;
1040
+ path: mutation.sourceFile,
1041
+ version,
1042
+ writeToken,
1043
+ overwrote: before
1044
+ });
1045
+ return {
1046
+ ...mutation,
1047
+ before,
1048
+ changed: true,
1049
+ backupPath: backupPathForResponse(projectDir, backup.backupPath),
1050
+ version,
1051
+ writeToken
1052
+ };
762
1053
  }
763
- function uniqueHostId(root, base) {
764
- const ids = /* @__PURE__ */ new Set([
765
- ...descendants(root, "[id]").map((element) => element.id),
766
- ...descendants(root, "[data-composition-id]").flatMap((element) => {
767
- const id = element.getAttribute("data-composition-id");
768
- return id ? [id] : [];
769
- })
770
- ]);
771
- if (!ids.has(base)) return base;
772
- let suffix = 2;
773
- while (ids.has(`${base}_${suffix}`)) suffix += 1;
774
- return `${base}_${suffix}`;
1054
+ var FileChangedError = class extends Error {
1055
+ constructor(sourceFile) {
1056
+ super("file changed since the timeline was read");
1057
+ this.sourceFile = sourceFile;
1058
+ }
1059
+ sourceFile;
1060
+ };
1061
+ function assertExpectedVersion(mutation, current) {
1062
+ const stale = mutation.expectedVersion === void 0 ? current !== mutation.before : fileContentVersion(current) !== mutation.expectedVersion;
1063
+ if (stale) throw new FileChangedError(mutation.sourceFile);
775
1064
  }
776
- function relativeSourcePath(targetAbs, sourceAbs) {
777
- return relative2(dirname2(targetAbs), sourceAbs).split(sep).join("/");
1065
+
1066
+ // src/routes/files.ts
1067
+ import { classifyPropertyGroup } from "@hyperframes/parsers/gsap-constants";
1068
+ import { findTimelineScript, parseGsapScriptAcorn } from "@hyperframes/parsers/gsap-parser-acorn";
1069
+ import { unrollComputedTimeline } from "@hyperframes/parsers";
1070
+ import {
1071
+ updateAnimationInScript,
1072
+ addAnimationToScript,
1073
+ removeAnimationFromScript,
1074
+ addKeyframeToScript,
1075
+ removeKeyframeFromScript,
1076
+ moveKeyframeInScript,
1077
+ resizeKeyframedTweenInScript,
1078
+ updateKeyframeInScript,
1079
+ convertToKeyframesFromScript,
1080
+ removeAllKeyframesFromScript,
1081
+ materializeKeyframesFromScript,
1082
+ unrollDynamicAnimations,
1083
+ setArcPathInScript,
1084
+ updateArcSegmentInScript,
1085
+ updateMotionPathPointInScript,
1086
+ addMotionPathPointInScript,
1087
+ removeMotionPathPointInScript,
1088
+ addMotionPathToScript,
1089
+ removeArcPathFromScript,
1090
+ addAnimationWithKeyframesToScript,
1091
+ replaceTweenWithKeyframesInScript,
1092
+ splitAnimationsInScript,
1093
+ splitIntoPropertyGroupsFromScript,
1094
+ retimeClipTweensInScript,
1095
+ dedupePositionWritesInScript,
1096
+ syncPositionHoldsBeforeKeyframes,
1097
+ trimTrailingKeyframeSpans,
1098
+ clipQueryRoot
1099
+ } from "@hyperframes/parsers/gsap-writer-acorn";
1100
+
1101
+ // src/helpers/studioFontFace.ts
1102
+ var FONT_STYLE_RE = /<style\b[^>]*data-hf-studio-fonts=(["'])true\1[^>]*>([\s\S]*?)<\/style>/i;
1103
+ var ONE_FONT_FACE_RULE = /^@font-face \{(?:[^{}<"'\\]|"(?:[^"\\<\n\r\f]|\\[^<\n\r\f])*"|'(?:[^'\\<\n\r\f]|\\[^<\n\r\f])*')*\}$/;
1104
+ function isStudioFontFaceCss(css) {
1105
+ return typeof css === "string" && ONE_FONT_FACE_RULE.test(css);
778
1106
  }
779
- function insertCompositionIntoSource(input) {
780
- const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
781
- const sourceAbs = canonicalProjectFile(input.projectDir, input.sourcePath);
782
- validateDependencyGraph(input.projectDir, targetAbs, sourceAbs);
783
- const source = readCompositionSource(sourceAbs);
784
- const sourceComposition = compositionRoot(source).root;
785
- const duration = positiveAttribute(
786
- sourceComposition,
787
- "data-composition-duration",
788
- "data-duration"
789
- );
790
- const width = positiveAttribute(sourceComposition, "data-width");
791
- const height = positiveAttribute(sourceComposition, "data-height");
792
- const { document, root } = compositionRoot(input.parentSource);
793
- const parentDuration = positiveAttribute(root, "data-duration", "data-composition-duration");
794
- const base = (sourceComposition.getAttribute("data-composition-id") ?? "composition").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "composition";
795
- const hostId = uniqueHostId(root, base);
796
- const track = resolveTrack(
797
- root,
798
- Math.max(0, Math.round(input.desiredTrack)),
799
- input.start,
800
- duration
801
- );
802
- const zIndex = Math.max(
803
- 0,
804
- ...descendants(root, "[style]").map((element) => {
805
- const match = /(?:^|;)\s*z-index\s*:\s*(-?\d+)/i.exec(element.getAttribute("style") ?? "");
806
- return match?.[1] ? Number.parseInt(match[1], 10) : 0;
807
- })
808
- ) + 1;
809
- const host = document.createElement("div");
810
- host.id = hostId;
811
- host.className = "clip";
812
- host.setAttribute("data-hf-id", `hf-${randomUUID()}`);
813
- host.setAttribute("data-composition-id", hostId);
814
- host.setAttribute("data-composition-src", relativeSourcePath(targetAbs, sourceAbs));
815
- host.setAttribute("data-start", String(Math.round(input.start * 100) / 100));
816
- host.setAttribute("data-duration", String(duration));
817
- host.setAttribute("data-playback-start", "0");
818
- host.setAttribute("data-track-index", String(track));
819
- host.setAttribute("data-width", String(width));
820
- host.setAttribute("data-height", String(height));
821
- host.setAttribute(
822
- "style",
823
- `position: absolute; left: 0px; top: 0px; width: ${width}px; height: ${height}px; z-index: ${zIndex}`
824
- );
825
- root.appendChild(host);
826
- if (input.start + duration > parentDuration) {
827
- const name = root.hasAttribute("data-duration") ? "data-duration" : "data-composition-duration";
828
- root.setAttribute(name, String(Math.round((input.start + duration) * 100) / 100));
1107
+ function ensureStudioFontFaceCss(html, css) {
1108
+ if (html.includes(css)) return html;
1109
+ const block = FONT_STYLE_RE.exec(html);
1110
+ if (block) {
1111
+ const next = `${(block[2] ?? "").trim()}
1112
+ ${css}`.trim();
1113
+ return html.replace(block[0], () => `<style data-hf-studio-fonts="true">
1114
+ ${next}
1115
+ </style>`);
829
1116
  }
830
- return { html: document.toString(), hostId, track, duration };
1117
+ const styleTag = `<style data-hf-studio-fonts="true">
1118
+ ${css}
1119
+ </style>`;
1120
+ if (/<\/head>/i.test(html)) return html.replace(/<\/head>/i, () => ` ${styleTag}
1121
+ </head>`);
1122
+ return `${styleTag}
1123
+ ${html}`;
831
1124
  }
832
1125
 
1126
+ // src/routes/files.ts
1127
+ import { parseHTML as parseHTML4 } from "linkedom";
1128
+ import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
1129
+
833
1130
  // src/routes/gsapMutationCapabilities.ts
834
1131
  var differential = (family) => ({
835
1132
  family,
@@ -946,7 +1243,7 @@ async function resolveProjectPath(c, adapter, route, opts) {
946
1243
  }
947
1244
  return { error: c.json({ error: "forbidden", why: "outside_project" }, 403) };
948
1245
  }
949
- if (opts?.mustExist && !existsSync3(absPath)) {
1246
+ if (opts?.mustExist && !existsSync4(absPath)) {
950
1247
  return { error: c.json({ error: "not found" }, 404) };
951
1248
  }
952
1249
  return { project, filePath, absPath };
@@ -1012,7 +1309,7 @@ function projectHtmlIds(projectDir) {
1012
1309
  if (!rel.endsWith(".html") || isInHiddenOrVendorDir(rel)) continue;
1013
1310
  let html;
1014
1311
  try {
1015
- html = readFileSync3(join5(projectDir, rel), "utf-8");
1312
+ html = readFileSync5(join6(projectDir, rel), "utf-8");
1016
1313
  } catch {
1017
1314
  continue;
1018
1315
  }
@@ -1041,7 +1338,7 @@ function foldAndCommitElementPatchBatches(projectDir, batches, writeFile, reques
1041
1338
  resolvedPaths.add(absPath);
1042
1339
  let before;
1043
1340
  try {
1044
- before = readFileSync3(absPath, "utf-8");
1341
+ before = readFileSync5(absPath, "utf-8");
1045
1342
  } catch {
1046
1343
  return { error: "not-found", sourceFile: batch.sourceFile };
1047
1344
  }
@@ -1097,7 +1394,7 @@ function commitElementPatchBatchesWithReceipts(c, projectDir, batches) {
1097
1394
  );
1098
1395
  }
1099
1396
  function writeFileWithReceipt(c, filePath, absPath, html) {
1100
- const overwrote = readFileSync3(absPath);
1397
+ const overwrote = readFileSync5(absPath);
1101
1398
  replaceFileAtomically2(absPath, html, statSync3(absPath).mode);
1102
1399
  const version = fileContentVersion(html);
1103
1400
  const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
@@ -1107,7 +1404,7 @@ function writeFileWithReceipt(c, filePath, absPath, html) {
1107
1404
  function writeMutationResult(c, projectDir, filePath, absPath, html, original) {
1108
1405
  const backup = snapshotBeforeWrite(projectDir, absPath);
1109
1406
  if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
1110
- if (readFileSync3(absPath, "utf-8") !== original) {
1407
+ if (readFileSync5(absPath, "utf-8") !== original) {
1111
1408
  return c.json({ error: "file changed", conflict: true, path: filePath }, 409);
1112
1409
  }
1113
1410
  const { version } = writeFileWithReceipt(c, filePath, absPath, html);
@@ -1162,7 +1459,7 @@ function generateCopyPath(projectDir, originalPath) {
1162
1459
  const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
1163
1460
  let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
1164
1461
  let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
1165
- while (existsSync3(resolve2(projectDir, candidate))) {
1462
+ while (existsSync4(resolve2(projectDir, candidate))) {
1166
1463
  num++;
1167
1464
  candidate = `${cleanBase} (copy ${num})${ext}`;
1168
1465
  }
@@ -1171,7 +1468,7 @@ function generateCopyPath(projectDir, originalPath) {
1171
1468
  function walkFiles(dir, filter) {
1172
1469
  const results = [];
1173
1470
  for (const entry of readableEntries(dir)) {
1174
- const full = join5(dir, entry.name);
1471
+ const full = join6(dir, entry.name);
1175
1472
  if (entry.isDirectory()) {
1176
1473
  if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders" || entry.name === ".transcode-cache")
1177
1474
  continue;
@@ -1196,7 +1493,7 @@ function readableEntries(dir) {
1196
1493
  }
1197
1494
  function readableText(file) {
1198
1495
  try {
1199
- return readFileSync3(file, "utf-8");
1496
+ return readFileSync5(file, "utf-8");
1200
1497
  } catch (error) {
1201
1498
  if (skippable(error)) return null;
1202
1499
  throw error;
@@ -1254,7 +1551,7 @@ function occurrences(text2, part) {
1254
1551
  }
1255
1552
  function projectPaths(root, dir = root, prefix = "", ancestors = /* @__PURE__ */ new Set()) {
1256
1553
  return readableEntries(dir).flatMap((entry) => {
1257
- const [path, full] = [`${prefix}${entry.name}`, join5(dir, entry.name)];
1554
+ const [path, full] = [`${prefix}${entry.name}`, join6(dir, entry.name)];
1258
1555
  const real = entry.name === "node_modules" ? null : directoryReal(entry, full);
1259
1556
  if (real === null || ancestors.has(real) || !isSafePath(root, full)) return [path];
1260
1557
  return [path, ...projectPaths(root, full, `${path}/`, /* @__PURE__ */ new Set([...ancestors, real]))];
@@ -1287,7 +1584,7 @@ function updateReferences(projectDir, oldPath, newPath, isDirectory) {
1287
1584
  return updatedCount;
1288
1585
  }
1289
1586
  function extractGsapScriptBlock(html) {
1290
- const { document } = parseHTML2(ensureHfIds(html));
1587
+ const { document } = parseHTML4(ensureHfIds(html));
1291
1588
  const scripts = [
1292
1589
  ...document.querySelectorAll("script:not([src])"),
1293
1590
  ...Array.from(document.querySelectorAll("template")).flatMap(
@@ -1562,7 +1859,7 @@ function containsRawGsapExpression(value) {
1562
1859
  return Object.values(value).some(containsRawGsapExpression);
1563
1860
  }
1564
1861
  async function prepareGsapMutationScript(c, res, firstMutation) {
1565
- const beforeHtml = readFileSync3(res.absPath, "utf-8");
1862
+ const beforeHtml = readFileSync5(res.absPath, "utf-8");
1566
1863
  let html = beforeHtml;
1567
1864
  let block = extractGsapScriptBlock(html);
1568
1865
  if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
@@ -1637,6 +1934,7 @@ async function applyGsapMutations(c, res, mutations) {
1637
1934
  const prepared = await prepareGsapMutationScript(c, res, firstMutation);
1638
1935
  if (prepared instanceof Response) return prepared;
1639
1936
  const { html, beforeHtml, block } = prepared;
1937
+ let lengthIsAuthored;
1640
1938
  const initialScript = block.scriptText;
1641
1939
  const skippedSelectors = /* @__PURE__ */ new Set();
1642
1940
  const respond = (data, status) => status ? c.json(data, status) : c.json(data);
@@ -1668,7 +1966,9 @@ async function applyGsapMutations(c, res, mutations) {
1668
1966
  for (const selector of result.skippedSelectors) skippedSelectors.add(selector);
1669
1967
  }
1670
1968
  if (HOLD_SYNC_MUTATION_TYPES.has(mutation.type)) {
1671
- newScript = writer === "acorn" ? syncPositionHoldsBeforeKeyframes(newScript) : (await loadGsapParser()).syncPositionHoldsBeforeKeyframes(newScript);
1969
+ lengthIsAuthored ??= !lengthIsTimeline(res.project.dir, res.filePath);
1970
+ if (lengthIsAuthored) newScript = trimTrailingKeyframeSpans(previousScript, newScript);
1971
+ newScript = writer === "acorn" ? syncPositionHoldsBeforeKeyframes(newScript, previousScript) : (await loadGsapParser()).syncPositionHoldsBeforeKeyframes(newScript, previousScript);
1672
1972
  }
1673
1973
  mutationChanges.push(newScript !== previousScript);
1674
1974
  block.scriptText = newScript;
@@ -1676,7 +1976,7 @@ async function applyGsapMutations(c, res, mutations) {
1676
1976
  const changed = block.scriptText !== initialScript;
1677
1977
  const newHtml = changed ? block.replaceScript(block.scriptText) : html;
1678
1978
  let backupPath = null;
1679
- if (readFileSync3(res.absPath, "utf-8") !== beforeHtml) {
1979
+ if (readFileSync5(res.absPath, "utf-8") !== beforeHtml) {
1680
1980
  return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
1681
1981
  }
1682
1982
  if (changed) {
@@ -2367,7 +2667,7 @@ async function foldAtomicCutFile(c, file, absPath, before, writer) {
2367
2667
  for (const selector of result.skippedSelectors) skippedSelectors.add(selector);
2368
2668
  }
2369
2669
  if (script !== block.scriptText) {
2370
- script = writer === "acorn" ? syncPositionHoldsBeforeKeyframes(script) : (await loadGsapParser()).syncPositionHoldsBeforeKeyframes(script);
2670
+ script = writer === "acorn" ? syncPositionHoldsBeforeKeyframes(script, block.scriptText) : (await loadGsapParser()).syncPositionHoldsBeforeKeyframes(script, block.scriptText);
2371
2671
  after = block.replaceScript(script);
2372
2672
  }
2373
2673
  }
@@ -2406,9 +2706,9 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2406
2706
  const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
2407
2707
  const MAX_COPY_INDEX = 1e4;
2408
2708
  let n = 1;
2409
- if (existsSync3(finalPath)) {
2709
+ if (existsSync4(finalPath)) {
2410
2710
  n = 2;
2411
- while (n < MAX_COPY_INDEX && existsSync3(resolve2(targetDir, `${base} (${n})${ext}`))) n++;
2711
+ while (n < MAX_COPY_INDEX && existsSync4(resolve2(targetDir, `${base} (${n})${ext}`))) n++;
2412
2712
  if (n >= MAX_COPY_INDEX) {
2413
2713
  skipped.push(name);
2414
2714
  continue;
@@ -2442,7 +2742,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2442
2742
  if (n >= MAX_COPY_INDEX) skipped.push(name);
2443
2743
  continue;
2444
2744
  }
2445
- const relativePath = subDir ? join5(subDir, finalName) : finalName;
2745
+ const relativePath = subDir ? join6(subDir, finalName) : finalName;
2446
2746
  uploaded.push(relativePath);
2447
2747
  if (validation.unchecked) unchecked.push({ name: finalName, reason: validation.unchecked });
2448
2748
  if (isAudioFile(finalName)) {
@@ -2475,7 +2775,7 @@ function registerFileRoutes(api, adapter) {
2475
2775
  if (stat2.size > MAX_TEXT_READ_BYTES) {
2476
2776
  return c.json({ error: "too large to read as text", why: "too_large" }, 413);
2477
2777
  }
2478
- const content = readFileSync3(fd);
2778
+ const content = readFileSync5(fd);
2479
2779
  const version = fileContentVersion(content);
2480
2780
  c.header("ETag", version);
2481
2781
  if (content.subarray(0, GIT_BINARY_SNIFF_BYTES).includes(0)) {
@@ -2500,7 +2800,7 @@ function registerFileRoutes(api, adapter) {
2500
2800
  if (expectedVersion === null && !createOnly) {
2501
2801
  let currentContent = null;
2502
2802
  try {
2503
- currentContent = readFileSync3(res.absPath);
2803
+ currentContent = readFileSync5(res.absPath);
2504
2804
  } catch (error) {
2505
2805
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
2506
2806
  throw error;
@@ -2526,7 +2826,7 @@ function registerFileRoutes(api, adapter) {
2526
2826
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
2527
2827
  throw error;
2528
2828
  }
2529
- const currentContent = readFileSync3(res.absPath);
2829
+ const currentContent = readFileSync5(res.absPath);
2530
2830
  return c.json(
2531
2831
  {
2532
2832
  error: "file conflict",
@@ -2556,7 +2856,7 @@ function registerFileRoutes(api, adapter) {
2556
2856
  );
2557
2857
  }
2558
2858
  try {
2559
- const currentContent = readFileSync3(fd);
2859
+ const currentContent = readFileSync5(fd);
2560
2860
  overwrote = currentContent;
2561
2861
  const currentVersion = fileContentVersion(currentContent);
2562
2862
  if (expectedVersion !== currentVersion) {
@@ -2632,7 +2932,7 @@ function registerFileRoutes(api, adapter) {
2632
2932
  }
2633
2933
  let before;
2634
2934
  try {
2635
- before = readFileSync3(ctx.absPath, "utf-8");
2935
+ before = readFileSync5(ctx.absPath, "utf-8");
2636
2936
  } catch (error) {
2637
2937
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
2638
2938
  throw error;
@@ -2661,7 +2961,7 @@ function registerFileRoutes(api, adapter) {
2661
2961
  }
2662
2962
  const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
2663
2963
  if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
2664
- const current = readFileSync3(ctx.absPath, "utf-8");
2964
+ const current = readFileSync5(ctx.absPath, "utf-8");
2665
2965
  if (current !== before) {
2666
2966
  const currentVersion2 = fileContentVersion(current);
2667
2967
  return c.json({ error: "file conflict", currentVersion: currentVersion2, currentContent: current }, 409);
@@ -2689,12 +2989,12 @@ function registerFileRoutes(api, adapter) {
2689
2989
  api.post("/projects/:id/file-mutations/remove-element/*", async (c) => {
2690
2990
  const ctx = await resolveFileMutationContext(c, adapter, "remove-element");
2691
2991
  if ("error" in ctx) return ctx.error;
2692
- if (!existsSync3(ctx.absPath)) {
2992
+ if (!existsSync4(ctx.absPath)) {
2693
2993
  return c.json({ error: "not found" }, 404);
2694
2994
  }
2695
2995
  const parsed = await parseMutationBody(c);
2696
2996
  if ("error" in parsed) return parsed.error;
2697
- const originalContent = readFileSync3(ctx.absPath, "utf-8");
2997
+ const originalContent = readFileSync5(ctx.absPath, "utf-8");
2698
2998
  return writeIfChanged(
2699
2999
  c,
2700
3000
  ctx.project.dir,
@@ -2707,7 +3007,7 @@ function registerFileRoutes(api, adapter) {
2707
3007
  api.post("/projects/:id/file-mutations/remove-elements/*", async (c) => {
2708
3008
  const ctx = await resolveFileMutationContext(c, adapter, "remove-elements");
2709
3009
  if ("error" in ctx) return ctx.error;
2710
- if (!existsSync3(ctx.absPath)) {
3010
+ if (!existsSync4(ctx.absPath)) {
2711
3011
  return c.json({ error: "not found" }, 404);
2712
3012
  }
2713
3013
  const body = await c.req.json().catch(() => null);
@@ -2715,7 +3015,7 @@ function registerFileRoutes(api, adapter) {
2715
3015
  if (!Array.isArray(targets) || targets.length === 0) {
2716
3016
  return c.json({ error: "targets required" }, 400);
2717
3017
  }
2718
- const originalContent = readFileSync3(ctx.absPath, "utf-8");
3018
+ const originalContent = readFileSync5(ctx.absPath, "utf-8");
2719
3019
  return writeIfChanged(
2720
3020
  c,
2721
3021
  ctx.project.dir,
@@ -2751,7 +3051,7 @@ function registerFileRoutes(api, adapter) {
2751
3051
  seen.add(absPath);
2752
3052
  let before;
2753
3053
  try {
2754
- before = readFileSync3(absPath, "utf-8");
3054
+ before = readFileSync5(absPath, "utf-8");
2755
3055
  } catch {
2756
3056
  return c.json({ error: `not found: ${file.path}` }, 404);
2757
3057
  }
@@ -2778,7 +3078,7 @@ function registerFileRoutes(api, adapter) {
2778
3078
  prepared.push(folded);
2779
3079
  }
2780
3080
  for (const file of prepared) {
2781
- const current = readFileSync3(file.absPath, "utf-8");
3081
+ const current = readFileSync5(file.absPath, "utf-8");
2782
3082
  if (current !== file.before) {
2783
3083
  return c.json(
2784
3084
  {
@@ -2821,7 +3121,7 @@ function registerFileRoutes(api, adapter) {
2821
3121
  const conflicts = [];
2822
3122
  for (const file of written.reverse()) {
2823
3123
  try {
2824
- const current = readFileSync3(file.absPath, "utf-8");
3124
+ const current = readFileSync5(file.absPath, "utf-8");
2825
3125
  if (current !== file.after) {
2826
3126
  conflicts.push(file.path);
2827
3127
  continue;
@@ -2869,7 +3169,7 @@ function registerFileRoutes(api, adapter) {
2869
3169
  const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
2870
3170
  let originalContent;
2871
3171
  try {
2872
- originalContent = readFileSync3(ctx.absPath, "utf-8");
3172
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2873
3173
  } catch {
2874
3174
  return c.json({ error: "not found" }, 404);
2875
3175
  }
@@ -2931,7 +3231,7 @@ function registerFileRoutes(api, adapter) {
2931
3231
  for (let attempt = 1; ; attempt += 1) {
2932
3232
  let originalContent;
2933
3233
  try {
2934
- originalContent = readFileSync3(ctx.absPath, "utf-8");
3234
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2935
3235
  } catch {
2936
3236
  return c.json({ error: "not found" }, 404);
2937
3237
  }
@@ -3043,7 +3343,7 @@ function registerFileRoutes(api, adapter) {
3043
3343
  }
3044
3344
  let originalContent;
3045
3345
  try {
3046
- originalContent = readFileSync3(ctx.absPath, "utf-8");
3346
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
3047
3347
  } catch {
3048
3348
  return c.json({ error: "not found" }, 404);
3049
3349
  }
@@ -3099,7 +3399,7 @@ function registerFileRoutes(api, adapter) {
3099
3399
  ).map((entry) => ({ target: entry.target, track: entry.track }));
3100
3400
  let originalContent;
3101
3401
  try {
3102
- originalContent = readFileSync3(ctx.absPath, "utf-8");
3402
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
3103
3403
  } catch {
3104
3404
  return c.json({ error: "not found" }, 404);
3105
3405
  }
@@ -3128,7 +3428,7 @@ function registerFileRoutes(api, adapter) {
3128
3428
  if ("error" in parsed) return parsed.error;
3129
3429
  let content;
3130
3430
  try {
3131
- content = readFileSync3(ctx.absPath, "utf-8");
3431
+ content = readFileSync5(ctx.absPath, "utf-8");
3132
3432
  } catch {
3133
3433
  return c.json({ exists: false });
3134
3434
  }
@@ -3142,7 +3442,7 @@ function registerFileRoutes(api, adapter) {
3142
3442
  if (!Array.isArray(body?.targets)) return c.json({ error: "targets required" }, 400);
3143
3443
  let content;
3144
3444
  try {
3145
- content = readFileSync3(ctx.absPath, "utf-8");
3445
+ content = readFileSync5(ctx.absPath, "utf-8");
3146
3446
  } catch {
3147
3447
  return c.json({ exists: body.targets.map(() => false) });
3148
3448
  }
@@ -3159,7 +3459,7 @@ function registerFileRoutes(api, adapter) {
3159
3459
  if (!newAbs) {
3160
3460
  return c.json({ error: "forbidden" }, 403);
3161
3461
  }
3162
- if (existsSync3(newAbs)) {
3462
+ if (existsSync4(newAbs)) {
3163
3463
  return c.json({ error: "already exists" }, 409);
3164
3464
  }
3165
3465
  const isDirectory = statSync3(res.absPath, { throwIfNoEntry: false })?.isDirectory() ?? false;
@@ -3176,7 +3476,7 @@ function registerFileRoutes(api, adapter) {
3176
3476
  return c.json({ error: "path required" }, 400);
3177
3477
  }
3178
3478
  const srcAbs = resolveWithinProject(project.dir, body.path);
3179
- if (!srcAbs || !existsSync3(srcAbs)) {
3479
+ if (!srcAbs || !existsSync4(srcAbs)) {
3180
3480
  return c.json({ error: "not found" }, 404);
3181
3481
  }
3182
3482
  const copyPath = generateCopyPath(project.dir, body.path);
@@ -3186,7 +3486,7 @@ function registerFileRoutes(api, adapter) {
3186
3486
  }
3187
3487
  ensureDir(project.dir, destAbs);
3188
3488
  try {
3189
- createFileAtomically(destAbs, readFileSync3(srcAbs));
3489
+ createFileAtomically(destAbs, readFileSync5(srcAbs));
3190
3490
  } catch (error) {
3191
3491
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
3192
3492
  throw error;
@@ -3230,7 +3530,7 @@ function registerFileRoutes(api, adapter) {
3230
3530
  mustExist: true
3231
3531
  });
3232
3532
  if ("error" in res) return res.error;
3233
- const html = readFileSync3(res.absPath, "utf-8");
3533
+ const html = readFileSync5(res.absPath, "utf-8");
3234
3534
  const etag = `${fileContentVersion(html).slice(0, -1)}:gsap-animations:v1"`;
3235
3535
  const headers = { ETag: etag, "Cache-Control": "no-cache" };
3236
3536
  if (c.req.header("If-None-Match") === etag) return new Response(null, { status: 304, headers });
@@ -3253,256 +3553,67 @@ function registerFileRoutes(api, adapter) {
3253
3553
  const res = await resolveProjectPath(c, adapter, "gsap-mutations", {
3254
3554
  mustExist: true,
3255
3555
  pin: true
3256
- });
3257
- if ("error" in res) return res.error;
3258
- const body = await c.req.json().catch(() => null);
3259
- if (!body) return c.json({ error: "mutation type required" }, 400);
3260
- const error = validateGsapMutationRequest(c, body);
3261
- if (error) return error;
3262
- return applyGsapMutations(c, res, [body]);
3263
- });
3264
- api.post("/projects/:id/gsap-mutations-batch/*", async (c) => {
3265
- const res = await resolveProjectPath(c, adapter, "gsap-mutations-batch", {
3266
- mustExist: true,
3267
- pin: true
3268
- });
3269
- if ("error" in res) return res.error;
3270
- const body = await c.req.json().catch(() => null);
3271
- if (!body || !Array.isArray(body.mutations) || body.mutations.length === 0) {
3272
- return c.json({ error: "mutations array required" }, 400);
3273
- }
3274
- for (const mutation of body.mutations) {
3275
- const error = validateGsapMutationRequest(c, mutation);
3276
- if (error) return error;
3277
- }
3278
- return applyGsapMutations(c, res, body.mutations);
3279
- });
3280
- api.post("/projects/:id/gsap-mutation-rollback/*", async (c) => {
3281
- const res = await resolveProjectPath(c, adapter, "gsap-mutation-rollback", {
3282
- mustExist: true,
3283
- pin: true
3284
- });
3285
- if ("error" in res) return res.error;
3286
- const body = await c.req.json().catch(() => null);
3287
- if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
3288
- return c.json({ error: "expected and restore contents required" }, 400);
3289
- }
3290
- const current = readFileSync3(res.absPath, "utf-8");
3291
- if (current !== body.expected) {
3292
- return c.json({ ok: true, restored: false, conflict: true });
3293
- }
3294
- replaceFileAtomically2(res.absPath, body.restore, statSync3(res.absPath).mode);
3295
- return c.json({ ok: true, restored: true, conflict: false });
3296
- });
3297
- }
3298
-
3299
- // src/routes/preview.ts
3300
- import { parseHTML as parseHTML5 } from "linkedom";
3301
- import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
3302
- import { Readable } from "stream";
3303
- import { join as join8, resolve as resolve4 } from "path";
3304
- import { createHash } from "crypto";
3305
- import {
3306
- AFTER_FONTS_SCRIPT_TYPE,
3307
- addScenePartsManifest,
3308
- RUNTIME_BOOTSTRAP_ATTR as RUNTIME_BOOTSTRAP_ATTR2,
3309
- insertBeforeCloseTag as insertBeforeCloseTag2,
3310
- insertRuntimeTag,
3311
- stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2
3312
- } from "@hyperframes/core/compiler";
3313
- import { STUDIO_PREVIEW_MARK_META } from "@hyperframes/core/studio-preview-mark";
3314
- import { gsapCdnDist as gsapCdnDist2, motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
3315
- import { findStartTags as findStartTags2, injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
3316
- import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
3317
-
3318
- // src/helpers/subComposition.ts
3319
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
3320
- import { join as join6 } from "path";
3321
- import { parseHTML as parseHTML3 } from "linkedom";
3322
- import {
3323
- rewriteAssetPaths,
3324
- rewriteCssAssetUrls,
3325
- rewriteInlineStyleAssetUrls
3326
- } from "@hyperframes/core";
3327
- import {
3328
- deferScriptsUntilFonts,
3329
- RUNTIME_BOOTSTRAP_ATTR,
3330
- stripEmbeddedRuntimeScripts
3331
- } from "@hyperframes/core/compiler";
3332
- import { isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
3333
- import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
3334
- function rewriteRelativePaths(root, compPath, projectDir) {
3335
- const assetExists = (path) => existsSync4(join6(projectDir, path));
3336
- rewriteAssetPaths(
3337
- root.querySelectorAll("[src], [href]"),
3338
- compPath,
3339
- (el, attr) => el.getAttribute(attr),
3340
- (el, attr, value) => el.setAttribute(attr, value),
3341
- assetExists
3342
- );
3343
- rewriteInlineStyleAssetUrls(
3344
- root.querySelectorAll("[style]"),
3345
- compPath,
3346
- (el) => el.getAttribute("style"),
3347
- (el, value) => el.setAttribute("style", value),
3348
- assetExists
3349
- );
3350
- for (const styleEl of root.querySelectorAll("style")) {
3351
- styleEl.textContent = rewriteCssAssetUrls(styleEl.textContent || "", compPath, assetExists);
3352
- }
3353
- }
3354
- function escapeLeadingDigitIdent(id) {
3355
- return `\\${id.charCodeAt(0).toString(16)} ${id.slice(1)}`;
3356
- }
3357
- var REGEXP_SPECIALS = /[.*+?^${}()|[\]\\]/g;
3358
- function fixDigitLeadingIdSelectors(root) {
3359
- const digitIds = /* @__PURE__ */ new Set();
3360
- for (const el of root.querySelectorAll("[id]")) {
3361
- const id = el.getAttribute("id");
3362
- if (id && /^\d/.test(id)) digitIds.add(id);
3363
- }
3364
- if (digitIds.size === 0) return;
3365
- for (const styleEl of root.querySelectorAll("style")) {
3366
- let css = styleEl.textContent || "";
3367
- for (const id of digitIds) {
3368
- const pattern = new RegExp(`#${id.replace(REGEXP_SPECIALS, "\\$&")}(?![\\w-])`, "g");
3369
- css = css.replace(pattern, `#${escapeLeadingDigitIdent(id)}`);
3556
+ });
3557
+ if ("error" in res) return res.error;
3558
+ const body = await c.req.json().catch(() => null);
3559
+ if (!body) return c.json({ error: "mutation type required" }, 400);
3560
+ const error = validateGsapMutationRequest(c, body);
3561
+ if (error) return error;
3562
+ return applyGsapMutations(c, res, [body]);
3563
+ });
3564
+ api.post("/projects/:id/gsap-mutations-batch/*", async (c) => {
3565
+ const res = await resolveProjectPath(c, adapter, "gsap-mutations-batch", {
3566
+ mustExist: true,
3567
+ pin: true
3568
+ });
3569
+ if ("error" in res) return res.error;
3570
+ const body = await c.req.json().catch(() => null);
3571
+ if (!body || !Array.isArray(body.mutations) || body.mutations.length === 0) {
3572
+ return c.json({ error: "mutations array required" }, 400);
3370
3573
  }
3371
- styleEl.textContent = css;
3372
- }
3373
- }
3374
- function extractFullDocumentParts(rawHtml, compPath, projectDir) {
3375
- const { document: doc } = parseHTML3(rawHtml);
3376
- const rewriteTargets = [doc.head, doc.body].filter(Boolean);
3377
- for (const target of rewriteTargets) {
3378
- rewriteRelativePaths(target, compPath, projectDir);
3379
- }
3380
- fixDigitLeadingIdSelectors(doc);
3381
- const headContent = doc.head?.innerHTML ?? "";
3382
- const bodyContent = doc.body?.innerHTML ?? "";
3383
- const htmlEl = doc.documentElement;
3384
- const htmlAttrs = extractElementAttrs(htmlEl);
3385
- const bodyAttrs = doc.body ? extractElementAttrs(doc.body) : "";
3386
- return { headContent, bodyContent, htmlAttrs, bodyAttrs };
3387
- }
3388
- function extractTemplateInnerHtml(rawComp) {
3389
- const { document: doc } = parseHTML3(rawComp);
3390
- const template = doc.querySelector("template");
3391
- return template ? template.innerHTML : null;
3392
- }
3393
- function escapeAttrValue(value) {
3394
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
3395
- }
3396
- function extractElementAttrs(el) {
3397
- const parts = [];
3398
- for (let i = 0; i < el.attributes.length; i++) {
3399
- const attr = el.attributes[i];
3400
- if (attr.value === "") {
3401
- parts.push(attr.name);
3402
- } else {
3403
- parts.push(`${attr.name}="${escapeAttrValue(attr.value)}"`);
3574
+ for (const mutation of body.mutations) {
3575
+ const error = validateGsapMutationRequest(c, mutation);
3576
+ if (error) return error;
3404
3577
  }
3405
- }
3406
- return parts.join(" ");
3407
- }
3408
- var NON_RENDERED_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "LINK", "META", "TEMPLATE", "NOSCRIPT"]);
3409
- function promoteTemplateCompositionId(rawComp, body) {
3410
- let templateCompositionId;
3411
- for (const tag of rawComp.matchAll(/<template\b[^>]*/gi)) {
3412
- const id = /\bdata-composition-id\s*=\s*["']([^"']+)["']/i.exec(tag[0] ?? "")?.[1];
3413
- if (id) {
3414
- templateCompositionId = id;
3415
- break;
3578
+ return applyGsapMutations(c, res, body.mutations);
3579
+ });
3580
+ api.post("/projects/:id/gsap-mutation-rollback/*", async (c) => {
3581
+ const res = await resolveProjectPath(c, adapter, "gsap-mutation-rollback", {
3582
+ mustExist: true,
3583
+ pin: true
3584
+ });
3585
+ if ("error" in res) return res.error;
3586
+ const body = await c.req.json().catch(() => null);
3587
+ if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
3588
+ return c.json({ error: "expected and restore contents required" }, 400);
3416
3589
  }
3417
- }
3418
- if (!templateCompositionId) return;
3419
- if (body.querySelector("[data-composition-id]")) return;
3420
- const root = Array.from(body.children).find((el) => !NON_RENDERED_TAGS.has(el.tagName));
3421
- root?.setAttribute("data-composition-id", templateCompositionId);
3422
- }
3423
- function tagRootCompositionFile(bodyHtml, compPath) {
3424
- const match = bodyHtml.match(/<[a-zA-Z][^>]*\bdata-composition-id=/);
3425
- if (match?.index == null) return bodyHtml;
3426
- const tagEnd = bodyHtml.indexOf(">", match.index);
3427
- if (tagEnd === -1) return bodyHtml;
3428
- if (bodyHtml.slice(match.index, tagEnd).includes("data-composition-file")) return bodyHtml;
3429
- return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
3430
- }
3431
- function rootHeadContent(rootHtml) {
3432
- return rootHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i)?.[1] ?? "";
3433
- }
3434
- function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
3435
- const compFile = join6(projectDir, compPath);
3436
- if (!existsSync4(compFile)) return null;
3437
- const rawComp = rawOverride ?? readFileSync4(compFile, "utf-8");
3438
- let compHeadContent = "";
3439
- let rewrittenContent;
3440
- let htmlAttrs = "";
3441
- let bodyAttrs = "";
3442
- const templateInner = extractTemplateInnerHtml(rawComp);
3443
- if (templateInner != null) {
3444
- const { document: contentDoc } = parseHTML3(
3445
- `<!DOCTYPE html><html><head></head><body>${templateInner}</body></html>`
3446
- );
3447
- rewriteRelativePaths(contentDoc, compPath, projectDir);
3448
- fixDigitLeadingIdSelectors(contentDoc);
3449
- promoteTemplateCompositionId(rawComp, contentDoc.body);
3450
- rewrittenContent = contentDoc.body.innerHTML || templateInner;
3451
- } else if (isFullHtmlDocument(rawComp)) {
3452
- const parts = extractFullDocumentParts(rawComp, compPath, projectDir);
3453
- compHeadContent = parts.headContent;
3454
- rewrittenContent = parts.bodyContent;
3455
- htmlAttrs = parts.htmlAttrs;
3456
- bodyAttrs = parts.bodyAttrs;
3457
- } else {
3458
- const { document: contentDoc } = parseHTML3(
3459
- `<!DOCTYPE html><html><head></head><body>${rawComp}</body></html>`
3460
- );
3461
- rewriteRelativePaths(contentDoc, compPath, projectDir);
3462
- fixDigitLeadingIdSelectors(contentDoc);
3463
- rewrittenContent = contentDoc.body.innerHTML || rawComp;
3464
- }
3465
- rewrittenContent = stripEmbeddedRuntimeScripts(rewrittenContent);
3466
- const { document: scriptsDoc } = parseHTML3(
3467
- `<!DOCTYPE html><html><head></head><body>${rewrittenContent}</body></html>`
3468
- );
3469
- deferScriptsUntilFonts(scriptsDoc);
3470
- rewrittenContent = scriptsDoc.body.innerHTML;
3471
- rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
3472
- const indexPath = join6(projectDir, "index.html");
3473
- let headContent = "";
3474
- if (existsSync4(indexPath)) headContent = rootHeadContent(readFileSync4(indexPath, "utf-8"));
3475
- if (baseHref && !hasBaseElement(headContent)) {
3476
- headContent = `<base href="${baseHref}">
3477
- ${headContent}`;
3478
- }
3479
- if (compHeadContent) headContent += `
3480
- ${compHeadContent}`;
3481
- headContent = stripEmbeddedRuntimeScripts(headContent);
3482
- headContent += `
3483
- <script ${RUNTIME_BOOTSTRAP_ATTR}="1" src="${runtimeUrl}"></script>`;
3484
- if (!headContent.includes("gsap")) {
3485
- headContent += `
3486
- <script src="${gsapCdnDist()}gsap.min.js"></script>`;
3487
- }
3488
- const htmlOpen = htmlAttrs ? `<html ${htmlAttrs}>` : "<html>";
3489
- const bodyOpen = bodyAttrs ? `<body ${bodyAttrs}>` : "<body>";
3490
- return `<!DOCTYPE html>
3491
- ${htmlOpen}
3492
- <head>
3493
- ${headContent}
3494
- </head>
3495
- ${bodyOpen}
3496
- <script>window.__timelines=window.__timelines||{};</script>
3497
- ${rewrittenContent}
3498
- </body>
3499
- </html>`;
3500
- }
3501
- function hasBaseElement(html) {
3502
- return parseHTML3(html).document.querySelector("base") !== null;
3590
+ const current = readFileSync5(res.absPath, "utf-8");
3591
+ if (current !== body.expected) {
3592
+ return c.json({ ok: true, restored: false, conflict: true });
3593
+ }
3594
+ replaceFileAtomically2(res.absPath, body.restore, statSync3(res.absPath).mode);
3595
+ return c.json({ ok: true, restored: true, conflict: false });
3596
+ });
3503
3597
  }
3504
3598
 
3505
3599
  // src/routes/preview.ts
3600
+ import { parseHTML as parseHTML6 } from "linkedom";
3601
+ import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
3602
+ import { Readable } from "stream";
3603
+ import { join as join8, resolve as resolve4 } from "path";
3604
+ import { createHash as createHash2 } from "crypto";
3605
+ import {
3606
+ AFTER_FONTS_SCRIPT_TYPE,
3607
+ addScenePartsManifest,
3608
+ RUNTIME_BOOTSTRAP_ATTR as RUNTIME_BOOTSTRAP_ATTR2,
3609
+ insertBeforeCloseTag as insertBeforeCloseTag2,
3610
+ insertRuntimeTag,
3611
+ stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2
3612
+ } from "@hyperframes/core/compiler";
3613
+ import { STUDIO_PREVIEW_MARK_META } from "@hyperframes/core/studio-preview-mark";
3614
+ import { gsapCdnDist as gsapCdnDist2, motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
3615
+ import { findStartTags as findStartTags2, injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
3616
+ import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
3506
3617
  import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
3507
3618
 
3508
3619
  // src/helpers/previewReads.ts
@@ -3606,7 +3717,7 @@ function injectPreviewVariables(html, values) {
3606
3717
 
3607
3718
  // src/helpers/lazyPreviewImages.ts
3608
3719
  import { findStartTags } from "@hyperframes/core/compiler/html-document";
3609
- import { parseHTML as parseHTML4 } from "linkedom";
3720
+ import { parseHTML as parseHTML5 } from "linkedom";
3610
3721
  import { STUDIO_PREVIEW_LAZY_ATTR } from "@hyperframes/core/studio-preview-mark";
3611
3722
  function startsAfterZero(el) {
3612
3723
  for (let node = el; node; node = node.parentElement) {
@@ -3617,7 +3728,7 @@ function startsAfterZero(el) {
3617
3728
  var hasLoading = (img) => Array.from(img.attributes).some((attr) => attr.name.toLowerCase() === "loading");
3618
3729
  function lazyPreviewImages(html) {
3619
3730
  if (!/<!doctype|<html[\s>]/i.test(html)) return html;
3620
- const images = [...parseHTML4(html).document.querySelectorAll("img")];
3731
+ const images = [...parseHTML5(html).document.querySelectorAll("img")];
3621
3732
  const tags = findStartTags(html, "img");
3622
3733
  if (tags.length !== images.length) return html;
3623
3734
  const parts = [];
@@ -3653,7 +3764,7 @@ function readStudioMotionManifestContent(projectDir) {
3653
3764
  const manifestPath = join8(projectDir, STUDIO_MOTION_PATH);
3654
3765
  if (!existsSync5(manifestPath)) return "";
3655
3766
  try {
3656
- return readFileSync5(manifestPath, "utf-8");
3767
+ return readFileSync6(manifestPath, "utf-8");
3657
3768
  } catch {
3658
3769
  return "";
3659
3770
  }
@@ -3708,7 +3819,7 @@ function isGsapCoreUrl(src) {
3708
3819
  return URL.canParse(src, base) && /\/gsap(\.min)?\.js$/i.test(new URL(src, base).pathname);
3709
3820
  }
3710
3821
  function findGsapCoreScript(html) {
3711
- const scripts = [...parseHTML5(html).document.querySelectorAll("script")];
3822
+ const scripts = [...parseHTML6(html).document.querySelectorAll("script")];
3712
3823
  const starts = findStartTags2(html, "script");
3713
3824
  if (starts.length !== scripts.length) return null;
3714
3825
  const at = scripts.findIndex(
@@ -3795,7 +3906,7 @@ function parsePreviewVariablesParam(raw) {
3795
3906
  }
3796
3907
  function variablesEtagSalt(raw) {
3797
3908
  if (!raw) return "";
3798
- return `:vars:${createHash("sha1").update(raw).digest("hex").slice(0, 12)}`;
3909
+ return `:vars:${createHash2("sha1").update(raw).digest("hex").slice(0, 12)}`;
3799
3910
  }
3800
3911
  function previewVariablesFromRequest(rawVariables) {
3801
3912
  const parse2 = parsePreviewVariablesParam(rawVariables);
@@ -3835,13 +3946,13 @@ function resolveProjectMainHtml(projectDir, projectId) {
3835
3946
  recordPreviewRead(projectDir, blockHtmlPath);
3836
3947
  if (existsSync5(indexPath)) {
3837
3948
  return {
3838
- html: readFileSync5(indexPath, "utf-8"),
3949
+ html: readFileSync6(indexPath, "utf-8"),
3839
3950
  compositionPath: "index.html"
3840
3951
  };
3841
3952
  }
3842
3953
  if (existsSync5(blockHtmlPath)) {
3843
3954
  return {
3844
- html: readFileSync5(blockHtmlPath, "utf-8"),
3955
+ html: readFileSync6(blockHtmlPath, "utf-8"),
3845
3956
  compositionPath: `${projectId}.html`
3846
3957
  };
3847
3958
  }
@@ -3972,7 +4083,7 @@ ${etag}`;
3972
4083
  function pinSubCompHfIds(compFile, compPath) {
3973
4084
  if (!/\.html?$/i.test(compPath)) return void 0;
3974
4085
  try {
3975
- return ensureHfIds2(readFileSync5(compFile, "utf-8"));
4086
+ return ensureHfIds2(readFileSync6(compFile, "utf-8"));
3976
4087
  } catch {
3977
4088
  return null;
3978
4089
  }
@@ -3991,7 +4102,7 @@ ${etag}`;
3991
4102
  if (!compFile || !existsSync5(compFile) || !statSync4(compFile).isFile()) {
3992
4103
  return c.text("not found", 404);
3993
4104
  }
3994
- const compPathHash = createHash("sha1").update(compPath).digest("hex");
4105
+ const compPathHash = createHash2("sha1").update(compPath).digest("hex");
3995
4106
  const capture = c.req.query(PREVIEW_CAPTURE_PARAM) !== void 0;
3996
4107
  const etag = `"comp:v2:${compPathHash}:${signature}${variablesEtagSalt(vars.raw)}${capture ? ":capture" : ""}"`;
3997
4108
  const ifNoneMatch = c.req.header("If-None-Match");
@@ -4083,7 +4194,7 @@ ${etag}`;
4083
4194
  recordProxyRequest();
4084
4195
  servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
4085
4196
  }
4086
- const textBuffer = isText ? Buffer.from(readFileSync5(file, "utf-8"), "utf-8") : null;
4197
+ const textBuffer = isText ? Buffer.from(readFileSync6(file, "utf-8"), "utf-8") : null;
4087
4198
  const totalSize = textBuffer ? textBuffer.length : statSync4(servedPath).size;
4088
4199
  const bodyFor = (start, end) => textBuffer ? new Uint8Array(textBuffer.subarray(start, end + 1)) : (
4089
4200
  // Node's web stream type and the DOM one do not overlap for tsc on
@@ -4127,7 +4238,7 @@ ${etag}`;
4127
4238
  }
4128
4239
 
4129
4240
  // src/routes/lint.ts
4130
- import { readFileSync as readFileSync6 } from "fs";
4241
+ import { readFileSync as readFileSync7 } from "fs";
4131
4242
  import { join as join9 } from "path";
4132
4243
  async function collectProjectFindings(adapter, projectDir) {
4133
4244
  const findings = [];
@@ -4146,7 +4257,7 @@ async function lintUncoveredHtml(adapter, projectDir, htmlFiles, coveredFiles) {
4146
4257
  const findings = [];
4147
4258
  for (const file of htmlFiles) {
4148
4259
  if (coveredFiles.has(file)) continue;
4149
- const content = readFileSync6(join9(projectDir, file), "utf-8");
4260
+ const content = readFileSync7(join9(projectDir, file), "utf-8");
4150
4261
  const result = await adapter.lint(content, { filePath: file, isSubComposition: true });
4151
4262
  for (const finding of result?.findings ?? []) {
4152
4263
  findings.push({ ...finding, file });
@@ -4179,7 +4290,7 @@ function registerLintRoutes(api, adapter) {
4179
4290
 
4180
4291
  // src/routes/render.ts
4181
4292
  import { streamSSE } from "hono/streaming";
4182
- import { existsSync as existsSync6, readFileSync as readFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
4293
+ import { existsSync as existsSync6, readFileSync as readFileSync8, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
4183
4294
  import { join as join10 } from "path";
4184
4295
  import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
4185
4296
  import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
@@ -4319,7 +4430,7 @@ function registerRenderRoutes(api, adapter) {
4319
4430
  }
4320
4431
  const contentType = renderContentType(job.outputPath);
4321
4432
  const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
4322
- const content = readFileSync7(job.outputPath);
4433
+ const content = readFileSync8(job.outputPath);
4323
4434
  return new Response(content, {
4324
4435
  headers: {
4325
4436
  "Content-Type": contentType,
@@ -4337,7 +4448,7 @@ function registerRenderRoutes(api, adapter) {
4337
4448
  }
4338
4449
  const contentType = renderContentType(job.outputPath);
4339
4450
  const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
4340
- const content = readFileSync7(job.outputPath);
4451
+ const content = readFileSync8(job.outputPath);
4341
4452
  return new Response(content, {
4342
4453
  headers: {
4343
4454
  "Content-Type": contentType,
@@ -4370,7 +4481,7 @@ function registerRenderRoutes(api, adapter) {
4370
4481
  if (!fp) return c.json({ error: "forbidden" }, 403);
4371
4482
  if (!existsSync6(fp)) return c.json({ error: "not found" }, 404);
4372
4483
  const contentType = renderContentType(fp);
4373
- const content = readFileSync7(fp);
4484
+ const content = readFileSync8(fp);
4374
4485
  return new Response(content, {
4375
4486
  headers: {
4376
4487
  "Content-Type": contentType,
@@ -4395,7 +4506,7 @@ function registerRenderRoutes(api, adapter) {
4395
4506
  let audioLoweredDb;
4396
4507
  if (existsSync6(metaPath)) {
4397
4508
  try {
4398
- const meta = JSON.parse(readFileSync7(metaPath, "utf-8"));
4509
+ const meta = JSON.parse(readFileSync8(metaPath, "utf-8"));
4399
4510
  if (meta.status === "failed" && !existsSync6(fp)) status = "failed";
4400
4511
  if (meta.durationMs) durationMs = meta.durationMs;
4401
4512
  if (typeof meta.audioLoweredDb === "number") audioLoweredDb = meta.audioLoweredDb;
@@ -4430,7 +4541,7 @@ function registerRenderRoutes(api, adapter) {
4430
4541
  // src/routes/imageThumbnail.ts
4431
4542
  import { HTTPException } from "hono/http-exception";
4432
4543
  import { open, stat } from "fs/promises";
4433
- import { createHash as createHash2 } from "crypto";
4544
+ import { createHash as createHash3 } from "crypto";
4434
4545
 
4435
4546
  // src/routes/thumbnailGenerationCoordinator.ts
4436
4547
  var ThumbnailGenerationCoordinator = class {
@@ -4612,7 +4723,7 @@ function registerImageThumbnailRoutes(api, adapter) {
4612
4723
  api.get("/projects/:id/image-thumbnail/*", async (c) => {
4613
4724
  try {
4614
4725
  const { path, info } = await resolveSource(adapter, c.req.param("id"), c.req.url);
4615
- const key = createHash2("sha256").update(JSON.stringify([path, info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs])).digest("hex");
4726
+ const key = createHash3("sha256").update(JSON.stringify([path, info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs])).digest("hex");
4616
4727
  const etag = `"${key}"`;
4617
4728
  c.header("ETag", etag);
4618
4729
  c.header("Cache-Control", "private, no-cache");
@@ -4653,90 +4764,6 @@ import {
4653
4764
  } from "fs";
4654
4765
  import { join as join11 } from "path";
4655
4766
  import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
4656
-
4657
- // src/helpers/compositionInputs.ts
4658
- import { readFileSync as readFileSync8 } from "fs";
4659
- import { createHash as createHash3 } from "crypto";
4660
- var ROOT_COMPOSITION = "index.html";
4661
- var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
4662
- function projectReader(projectDir) {
4663
- const sources = /* @__PURE__ */ new Map();
4664
- return (compPath) => {
4665
- if (!sources.has(compPath)) {
4666
- const file = resolveWithinProject(projectDir, compPath);
4667
- let source = null;
4668
- try {
4669
- source = file ? readFileSync8(file, "utf-8") : null;
4670
- } catch {
4671
- }
4672
- sources.set(compPath, source);
4673
- }
4674
- return sources.get(compPath) ?? null;
4675
- };
4676
- }
4677
- function normalizeSource(src) {
4678
- if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) return null;
4679
- const path = src.split(/[?#]/)[0]?.replace(/^\.?\//, "") ?? "";
4680
- return path || null;
4681
- }
4682
- function closureOf(read, compPath) {
4683
- const closure = /* @__PURE__ */ new Set();
4684
- const visit = (path) => {
4685
- if (closure.has(path)) return;
4686
- closure.add(path);
4687
- for (const match of (read(path) ?? "").matchAll(COMPOSITION_SRC)) {
4688
- const mounted = normalizeSource(match[2] ?? "");
4689
- if (mounted) visit(mounted);
4690
- }
4691
- };
4692
- visit(compPath);
4693
- return closure;
4694
- }
4695
- var inputSignatures = /* @__PURE__ */ new Map();
4696
- var rootMemos = /* @__PURE__ */ new Map();
4697
- var sha1 = (text2) => createHash3("sha1").update(text2).digest("hex");
4698
- function rootHashes(read) {
4699
- const source = read(ROOT_COMPOSITION) ?? "";
4700
- return { root: sha1(source), head: sha1(rootHeadContent(source)) };
4701
- }
4702
- function compositionInputSignature(projectDir, compPath, projectSignature) {
4703
- const key = `${projectDir}\0${compPath}`;
4704
- const known = inputSignatures.get(key);
4705
- if (known?.projectSignature === projectSignature) return known.inputSignature;
4706
- const read = projectReader(projectDir);
4707
- const comp = normalizeSource(compPath) ?? compPath;
4708
- const inputs = closureOf(read, comp).add(ROOT_COMPOSITION);
4709
- const excluded = new Set(
4710
- [...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path))
4711
- );
4712
- let inputSignature;
4713
- if (comp === ROOT_COMPOSITION) {
4714
- inputSignature = createProjectSignature(projectDir, excluded);
4715
- } else {
4716
- const hashes = rootHashes(read);
4717
- if (!rootMemos.has(projectDir)) rootMemos.set(projectDir, { ...hashes, affected: null });
4718
- inputSignature = `${createProjectSignature(projectDir, excluded.add(ROOT_COMPOSITION))}:${hashes.head}`;
4719
- }
4720
- inputSignatures.set(key, { projectSignature, inputSignature });
4721
- return inputSignature;
4722
- }
4723
- function compositionsAffectedBy(projectDir, changedPath) {
4724
- const changed = changedPath.replace(/\\/g, "/");
4725
- const read = projectReader(projectDir);
4726
- if (changed === ROOT_COMPOSITION) {
4727
- const now = rootHashes(read);
4728
- const memo = rootMemos.get(projectDir);
4729
- if (memo?.root === now.root) return memo.affected;
4730
- const affected = memo?.head === now.head ? [ROOT_COMPOSITION] : null;
4731
- rootMemos.set(projectDir, { ...now, affected });
4732
- return affected;
4733
- }
4734
- const mounted = closureOf(read, ROOT_COMPOSITION);
4735
- if (!mounted.has(changed)) return null;
4736
- return [...mounted].filter((path) => closureOf(read, path).has(changed));
4737
- }
4738
-
4739
- // src/routes/thumbnail.ts
4740
4767
  var THUMBNAIL_CACHE_VERSION = "v5";
4741
4768
  var THUMBNAIL_MAX_OUTPUT_WIDTH = 240;
4742
4769
  var THUMBNAIL_MAX_OUTPUT_HEIGHT = 135;