@hyperframes/studio-server 0.8.113 → 0.8.115

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/helpers/screenshotClip.ts"],"sourcesContent":["export interface ScreenshotClip {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n// Serialised by page.evaluate, so self-contained; fades everything but the element's own branch.\nexport function getElementScreenshotClip(\n selector: string,\n selectorIndex?: number,\n): ScreenshotClip | undefined {\n // Guard against invalid CSS selectors (e.g. `#0` — a digit-leading id from\n // user HTML that upstream producers forgot to CSS.escape). querySelectorAll\n // throws SyntaxError on those, which bubbles out of page.evaluate and fails\n // the whole thumbnail. Returning undefined here falls back to a full-page\n // screenshot, so the user still sees a thumbnail instead of a broken image.\n let matches: HTMLElement[];\n try {\n matches = Array.from(document.querySelectorAll(selector)).filter(\n (el): el is HTMLElement => el instanceof HTMLElement,\n );\n } catch {\n return undefined;\n }\n const safeIndex = Math.max(0, Math.min(matches.length - 1, Math.floor(selectorIndex ?? 0)));\n const el = matches[safeIndex] ?? null;\n if (!(el instanceof HTMLElement)) return undefined;\n // Opacity, not visibility; each prior inline opacity is kept for clearElementScreenshotIsolation.\n const page = window as Window & { __hfThumbnailFaded?: [CSSStyleDeclaration, string, string][] };\n const faded = (page.__hfThumbnailFaded ??= []);\n for (let node: Element = el; node.parentElement; node = node.parentElement) {\n for (const sibling of Array.from(node.parentElement.children)) {\n const style = (sibling as HTMLElement).style;\n if (sibling === node || !style) continue;\n faded.push([style, style.getPropertyValue(\"opacity\"), style.getPropertyPriority(\"opacity\")]);\n style.setProperty(\"opacity\", \"0\", \"important\");\n }\n }\n const rect = el.getBoundingClientRect();\n if (rect.width < 4 || rect.height < 4) return undefined;\n const pad = 8;\n const x = Math.max(0, rect.left - pad);\n const y = Math.max(0, rect.top - pad);\n const maxWidth = window.innerWidth - x;\n const maxHeight = window.innerHeight - y;\n return {\n x,\n y,\n width: Math.max(1, Math.min(rect.width + pad * 2, maxWidth)),\n height: Math.max(1, Math.min(rect.height + pad * 2, maxHeight)),\n };\n}\n\nexport function clearElementScreenshotIsolation(): void {\n const page = window as Window & { __hfThumbnailFaded?: [CSSStyleDeclaration, string, string][] };\n for (const [style, value, priority] of (page.__hfThumbnailFaded ?? []).reverse()) {\n if (value) style.setProperty(\"opacity\", value, priority);\n else style.removeProperty(\"opacity\");\n }\n page.__hfThumbnailFaded = [];\n}\n"],"mappings":";AAQO,SAAS,yBACd,UACA,eAC4B;AAM5B,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,KAAK,SAAS,iBAAiB,QAAQ,CAAC,EAAE;AAAA,MACxD,CAACA,QAA0BA,eAAc;AAAA,IAC3C;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,SAAS,GAAG,KAAK,MAAM,iBAAiB,CAAC,CAAC,CAAC;AAC1F,QAAM,KAAK,QAAQ,SAAS,KAAK;AACjC,MAAI,EAAE,cAAc,aAAc,QAAO;AAEzC,QAAM,OAAO;AACb,QAAM,QAAS,KAAK,uBAAuB,CAAC;AAC5C,WAAS,OAAgB,IAAI,KAAK,eAAe,OAAO,KAAK,eAAe;AAC1E,eAAW,WAAW,MAAM,KAAK,KAAK,cAAc,QAAQ,GAAG;AAC7D,YAAM,QAAS,QAAwB;AACvC,UAAI,YAAY,QAAQ,CAAC,MAAO;AAChC,YAAM,KAAK,CAAC,OAAO,MAAM,iBAAiB,SAAS,GAAG,MAAM,oBAAoB,SAAS,CAAC,CAAC;AAC3F,YAAM,YAAY,WAAW,KAAK,WAAW;AAAA,IAC/C;AAAA,EACF;AACA,QAAM,OAAO,GAAG,sBAAsB;AACtC,MAAI,KAAK,QAAQ,KAAK,KAAK,SAAS,EAAG,QAAO;AAC9C,QAAM,MAAM;AACZ,QAAM,IAAI,KAAK,IAAI,GAAG,KAAK,OAAO,GAAG;AACrC,QAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,GAAG;AACpC,QAAM,WAAW,OAAO,aAAa;AACrC,QAAM,YAAY,OAAO,cAAc;AACvC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,QAAQ,MAAM,GAAG,QAAQ,CAAC;AAAA,IAC3D,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,SAAS,MAAM,GAAG,SAAS,CAAC;AAAA,EAChE;AACF;AAEO,SAAS,kCAAwC;AACtD,QAAM,OAAO;AACb,aAAW,CAAC,OAAO,OAAO,QAAQ,MAAM,KAAK,sBAAsB,CAAC,GAAG,QAAQ,GAAG;AAChF,QAAI,MAAO,OAAM,YAAY,WAAW,OAAO,QAAQ;AAAA,QAClD,OAAM,eAAe,SAAS;AAAA,EACrC;AACA,OAAK,qBAAqB,CAAC;AAC7B;","names":["el"]}
@@ -5,5 +5,6 @@ interface ScreenshotClip {
5
5
  height: number;
6
6
  }
7
7
  declare function getElementScreenshotClip(selector: string, selectorIndex?: number): ScreenshotClip | undefined;
8
+ declare function clearElementScreenshotIsolation(): void;
8
9
 
9
- export { type ScreenshotClip, getElementScreenshotClip };
10
+ export { type ScreenshotClip, clearElementScreenshotIsolation, getElementScreenshotClip };
@@ -1,7 +1,9 @@
1
1
  import {
2
+ clearElementScreenshotIsolation,
2
3
  getElementScreenshotClip
3
- } from "../chunk-W2SBTCO2.js";
4
+ } from "../chunk-IMG2GSY6.js";
4
5
  export {
6
+ clearElementScreenshotIsolation,
5
7
  getElementScreenshotClip
6
8
  };
7
9
  //# sourceMappingURL=screenshotClip.js.map
@@ -9,6 +9,11 @@ declare function parseSourceDocument(raw: string): {
9
9
  wrappedFragment: boolean;
10
10
  };
11
11
  declare function findTargetElement(document: Document, target: SourceMutationTarget): Element | null;
12
+ /**
13
+ * Removes every target in one parse and one serialization. A target nested inside one already
14
+ * removed no longer matches, which is a normal outcome rather than a failure.
15
+ */
16
+ declare function removeElementsFromHtml(source: string, targets: readonly SourceMutationTarget[]): string;
12
17
  declare function removeElementFromHtml(source: string, target: SourceMutationTarget): string;
13
18
  declare function isHTMLElement(el: Node): el is HTMLElement;
14
19
  declare function dedupeClonedCompositionId(document: Document, clone: Element): void;
@@ -23,6 +28,8 @@ declare function patchElementInHtml(source: string, target: SourceMutationTarget
23
28
  html: string;
24
29
  matched: boolean;
25
30
  };
31
+ /** Whether each target exists in `source`; the document is parsed once however many targets ask. */
32
+ declare function probeElementsInSource(source: string, targets: readonly SourceMutationTarget[]): boolean[];
26
33
  declare function probeElementInSource(source: string, target: SourceMutationTarget): boolean;
27
34
  interface SplitElementResult {
28
35
  html: string;
@@ -87,4 +94,4 @@ declare function unwrapElementsFromHtml(source: string, groupTarget: SourceMutat
87
94
  /** After a cut, give each linked group's right halves their own `data-link` and `data-sync-origin`. */
88
95
  declare function relinkSplitHalvesInHtml(source: string, rightHalfIds: readonly string[]): string;
89
96
 
90
- export { type ElementRebase, type PatchOperation, type SourceMutationTarget, type SplitElementResult, type UnwrapChildTrack, type UnwrapElementsResult, type WrapElementsResult, dedupeClonedCompositionId, findTargetElement, isHTMLElement, parseSourceDocument, patchElementInHtml, probeElementInSource, relinkSplitHalvesInHtml, removeElementFromHtml, splitElementInHtml, unwrapElementsFromHtml, wrapElementsInHtml };
97
+ export { type ElementRebase, type PatchOperation, type SourceMutationTarget, type SplitElementResult, type UnwrapChildTrack, type UnwrapElementsResult, type WrapElementsResult, dedupeClonedCompositionId, findTargetElement, isHTMLElement, parseSourceDocument, patchElementInHtml, probeElementInSource, probeElementsInSource, relinkSplitHalvesInHtml, removeElementFromHtml, removeElementsFromHtml, splitElementInHtml, unwrapElementsFromHtml, wrapElementsInHtml };
@@ -5,12 +5,14 @@ import {
5
5
  parseSourceDocument,
6
6
  patchElementInHtml,
7
7
  probeElementInSource,
8
+ probeElementsInSource,
8
9
  relinkSplitHalvesInHtml,
9
10
  removeElementFromHtml,
11
+ removeElementsFromHtml,
10
12
  splitElementInHtml,
11
13
  unwrapElementsFromHtml,
12
14
  wrapElementsInHtml
13
- } from "../chunk-Z4DIWF3Z.js";
15
+ } from "../chunk-G4YB7Q2Q.js";
14
16
  export {
15
17
  dedupeClonedCompositionId,
16
18
  findTargetElement,
@@ -18,8 +20,10 @@ export {
18
20
  parseSourceDocument,
19
21
  patchElementInHtml,
20
22
  probeElementInSource,
23
+ probeElementsInSource,
21
24
  relinkSplitHalvesInHtml,
22
25
  removeElementFromHtml,
26
+ removeElementsFromHtml,
23
27
  splitElementInHtml,
24
28
  unwrapElementsFromHtml,
25
29
  wrapElementsInHtml
@@ -10,7 +10,7 @@ import {
10
10
  listProjectHistories,
11
11
  openProjectHistory,
12
12
  pruneGoneProjectHistories
13
- } from "../chunk-EPL4YKGI.js";
13
+ } from "../chunk-E3HLZKPI.js";
14
14
  import "../chunk-TTWLTYSQ.js";
15
15
  export {
16
16
  DEFAULT_HISTORY_ROOT,
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { walkDir } from './helpers/safePath.js';
8
8
  export { stampProjectHfIds } from './helpers/hfIdPersist.js';
9
9
  import { SourceMutationTarget } from './helpers/sourceMutation.js';
10
10
  export { PatchOperation, dedupeClonedCompositionId, findTargetElement, parseSourceDocument, patchElementInHtml, removeElementFromHtml, splitElementInHtml } from './helpers/sourceMutation.js';
11
- export { ScreenshotClip, getElementScreenshotClip } from './helpers/screenshotClip.js';
11
+ export { ScreenshotClip, clearElementScreenshotIsolation, getElementScreenshotClip } from './helpers/screenshotClip.js';
12
12
  export { STUDIO_MANUAL_EDITS_PATH, StudioManualEditsRenderScriptOptions, createStudioManualEditsRenderBodyScript, createStudioPositionSeekReapplyScript } from './helpers/manualEditsRenderScript.js';
13
13
  export { STUDIO_MOTION_PATH, StudioMotionRenderScriptOptions, createStudioMotionRenderBodyScript } from './helpers/studioMotionRenderScript.js';
14
14
  export { isSafePath } from '@hyperframes/core/safe-path';
@@ -54,7 +54,8 @@ declare function createProjectSignature(projectDir: string, excluding?: Readonly
54
54
 
55
55
  /**
56
56
  * Compositions whose rendered frames a write at `changedPath` can change, or `null` for all
57
- * of them: the root, assets and any file the root does not mount reach every composition.
57
+ * of them: assets, a root head edit, and any file the root does not mount reach every
58
+ * composition. A head-preserving root write changes only the root's own frames.
58
59
  */
59
60
  declare function compositionsAffectedBy(projectDir: string, changedPath: string): string[] | null;
60
61
 
package/dist/index.js CHANGED
@@ -9,12 +9,14 @@ import {
9
9
  parseSourceDocument,
10
10
  patchElementInHtml,
11
11
  probeElementInSource,
12
+ probeElementsInSource,
12
13
  relinkSplitHalvesInHtml,
13
14
  removeElementFromHtml,
15
+ removeElementsFromHtml,
14
16
  splitElementInHtml,
15
17
  unwrapElementsFromHtml,
16
18
  wrapElementsInHtml
17
- } from "./chunk-Z4DIWF3Z.js";
19
+ } from "./chunk-G4YB7Q2Q.js";
18
20
  import {
19
21
  isCompositionSource,
20
22
  stampProjectHfIds
@@ -44,10 +46,11 @@ import {
44
46
  resolveProjectAndSignature,
45
47
  resolveProjectSignature,
46
48
  settledFileTag
47
- } from "./chunk-EPL4YKGI.js";
49
+ } from "./chunk-E3HLZKPI.js";
48
50
  import {
51
+ clearElementScreenshotIsolation,
49
52
  getElementScreenshotClip
50
- } from "./chunk-W2SBTCO2.js";
53
+ } from "./chunk-IMG2GSY6.js";
51
54
  import {
52
55
  DEFAULT_TARGET_LUFS,
53
56
  absoluteLoudnessPlan,
@@ -165,7 +168,8 @@ import {
165
168
  statSync as statSync3,
166
169
  fstatSync,
167
170
  renameSync as renameSync2,
168
- readdirSync as readdirSync2
171
+ readdirSync as readdirSync2,
172
+ realpathSync
169
173
  } from "fs";
170
174
  import { resolve as resolve2, dirname as dirname3, join as join5 } from "path";
171
175
 
@@ -1179,18 +1183,83 @@ function readableText(file) {
1179
1183
  throw error;
1180
1184
  }
1181
1185
  }
1182
- function updateReferences(projectDir, oldPath, newPath) {
1186
+ var escapeRegExp = (text2) => text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1187
+ var SEPARATOR = String.raw`\\{0,2}[\\/]`;
1188
+ var REFERENCE_START = String.raw`(?<![\w./\\+-])`;
1189
+ var FILE_END = String.raw`(?![\w-]|\.\w)`;
1190
+ function referencePattern(oldPath, isDirectory) {
1191
+ const name = oldPath.split("/").map(escapeRegExp).join(SEPARATOR);
1192
+ const end = isDirectory ? `(?=${SEPARATOR})` : FILE_END;
1193
+ return new RegExp(
1194
+ String.raw`${REFERENCE_START}(?<lead>(?:\.{1,2}${SEPARATOR}|${SEPARATOR}){0,4})${name}${end}`,
1195
+ "g"
1196
+ );
1197
+ }
1198
+ function referenceRewriter(oldPath, newPath, isDirectory, existing = []) {
1199
+ const pattern = referencePattern(oldPath, isDirectory);
1200
+ const around = /* @__PURE__ */ new Map();
1201
+ const afterLengths = /* @__PURE__ */ new Set();
1202
+ let window = 0;
1203
+ for (const path of existing) {
1204
+ if (path.length <= oldPath.length) continue;
1205
+ for (const start of occurrences(path, oldPath)) {
1206
+ const [before, after] = [path.slice(0, start), path.slice(start + oldPath.length)];
1207
+ const befores = around.get(after) ?? around.set(after, /* @__PURE__ */ new Map()).get(after);
1208
+ (befores.get(before.length) ?? befores.set(before.length, /* @__PURE__ */ new Set()).get(before.length)).add(
1209
+ before
1210
+ );
1211
+ afterLengths.add(after.length);
1212
+ window = Math.max(window, path.length * 3);
1213
+ }
1214
+ }
1215
+ const normalized = (text2) => text2.replace(/\\{0,2}[\\/]/g, "/");
1216
+ return (text2) => text2.replace(pattern, (...args) => {
1217
+ const [match, offset] = [args[0], args.at(-3)];
1218
+ const { lead } = args.at(-1);
1219
+ const at = offset + lead.length;
1220
+ const head = normalized(text2.slice(Math.max(0, at - window), at));
1221
+ const tail = normalized(text2.slice(offset + match.length, offset + match.length + window));
1222
+ const inLonger = [...afterLengths].some((after) => {
1223
+ const befores = tail.length >= after && around.get(tail.slice(0, after));
1224
+ return befores && [...befores].some(
1225
+ ([length, set]) => head.length >= length && set.has(head.slice(head.length - length))
1226
+ );
1227
+ });
1228
+ return inLonger ? match : `${lead}${newPath}`;
1229
+ });
1230
+ }
1231
+ function occurrences(text2, part) {
1232
+ const at = [];
1233
+ for (let i = text2.indexOf(part); i >= 0; i = text2.indexOf(part, i + 1)) at.push(i);
1234
+ return at;
1235
+ }
1236
+ function projectPaths(root, dir = root, prefix = "", ancestors = /* @__PURE__ */ new Set()) {
1237
+ return readableEntries(dir).flatMap((entry) => {
1238
+ const [path, full] = [`${prefix}${entry.name}`, join5(dir, entry.name)];
1239
+ const real = entry.name === "node_modules" ? null : directoryReal(entry, full);
1240
+ if (real === null || ancestors.has(real) || !isSafePath(root, full)) return [path];
1241
+ return [path, ...projectPaths(root, full, `${path}/`, /* @__PURE__ */ new Set([...ancestors, real]))];
1242
+ });
1243
+ }
1244
+ function directoryReal(entry, full) {
1245
+ try {
1246
+ return entry.isDirectory() || entry.isSymbolicLink() && statSync3(full).isDirectory() ? realpathSync(full) : null;
1247
+ } catch {
1248
+ return null;
1249
+ }
1250
+ }
1251
+ function updateReferences(projectDir, oldPath, newPath, isDirectory) {
1183
1252
  const textFiles = walkFiles(
1184
1253
  projectDir,
1185
1254
  (name) => /\.(html|css|js|jsx|ts|tsx|json|mjs|cjs|md|mdx)$/i.test(name)
1186
1255
  );
1256
+ const rewrite = referenceRewriter(oldPath, newPath, isDirectory, projectPaths(projectDir));
1187
1257
  let updatedCount = 0;
1188
1258
  for (const file of textFiles) {
1189
1259
  if (!isSafePath(projectDir, file)) continue;
1190
1260
  const content = readableText(file);
1191
1261
  if (content === null) continue;
1192
- if (!content.includes(oldPath)) continue;
1193
- const updated = content.split(oldPath).join(newPath);
1262
+ const updated = rewrite(content);
1194
1263
  if (updated !== content) {
1195
1264
  replaceFileAtomically2(file, updated, statSync3(file).mode);
1196
1265
  updatedCount++;
@@ -1512,7 +1581,9 @@ async function applyGsapMutations(c, res, mutations) {
1512
1581
  } catch (error) {
1513
1582
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
1514
1583
  }
1584
+ const mutationChanges = [];
1515
1585
  for (const mutation of mutations) {
1586
+ const previousScript = block.scriptText;
1516
1587
  const result = await executeGsapMutation(mutation, block, respond, writer);
1517
1588
  if (result instanceof Response) return result;
1518
1589
  let newScript = typeof result === "string" ? result : result.script;
@@ -1522,6 +1593,7 @@ async function applyGsapMutations(c, res, mutations) {
1522
1593
  if (HOLD_SYNC_MUTATION_TYPES.has(mutation.type)) {
1523
1594
  newScript = writer === "acorn" ? syncPositionHoldsBeforeKeyframes(newScript) : (await loadGsapParser()).syncPositionHoldsBeforeKeyframes(newScript);
1524
1595
  }
1596
+ mutationChanges.push(newScript !== previousScript);
1525
1597
  block.scriptText = newScript;
1526
1598
  }
1527
1599
  const changed = block.scriptText !== initialScript;
@@ -1546,6 +1618,7 @@ async function applyGsapMutations(c, res, mutations) {
1546
1618
  ok: true,
1547
1619
  changed,
1548
1620
  mutated: changed,
1621
+ mutationChanges,
1549
1622
  parsed: parseGsapScriptAcorn(block.scriptText),
1550
1623
  before: beforeHtml,
1551
1624
  after: newHtml,
@@ -2605,11 +2678,14 @@ function registerFileRoutes(api, adapter) {
2605
2678
  return c.json({ error: "targets required" }, 400);
2606
2679
  }
2607
2680
  const originalContent = readFileSync3(ctx.absPath, "utf-8");
2608
- let next = originalContent;
2609
- for (const target of targets) {
2610
- next = removeElementFromHtml(next, target);
2611
- }
2612
- return writeIfChanged(c, ctx.project.dir, ctx.filePath, ctx.absPath, originalContent, next);
2681
+ return writeIfChanged(
2682
+ c,
2683
+ ctx.project.dir,
2684
+ ctx.filePath,
2685
+ ctx.absPath,
2686
+ originalContent,
2687
+ removeElementsFromHtml(originalContent, targets)
2688
+ );
2613
2689
  });
2614
2690
  api.post("/projects/:id/file-mutations/split-batch", async (c) => {
2615
2691
  const body = await c.req.json().catch(() => null);
@@ -3013,6 +3089,19 @@ function registerFileRoutes(api, adapter) {
3013
3089
  const exists = probeElementInSource(content, parsed.target);
3014
3090
  return c.json({ exists });
3015
3091
  });
3092
+ api.post("/projects/:id/file-mutations/probe-elements/*", async (c) => {
3093
+ const ctx = await resolveFileMutationContext(c, adapter, "probe-elements");
3094
+ if ("error" in ctx) return ctx.error;
3095
+ const body = await c.req.json().catch(() => null);
3096
+ if (!Array.isArray(body?.targets)) return c.json({ error: "targets required" }, 400);
3097
+ let content;
3098
+ try {
3099
+ content = readFileSync3(ctx.absPath, "utf-8");
3100
+ } catch {
3101
+ return c.json({ exists: body.targets.map(() => false) });
3102
+ }
3103
+ return c.json({ exists: probeElementsInSource(content, body.targets) });
3104
+ });
3016
3105
  api.patch("/projects/:id/files/*", async (c) => {
3017
3106
  const res = await resolveProjectFile(c, adapter, { mustExist: true });
3018
3107
  if ("error" in res) return res.error;
@@ -3027,9 +3116,10 @@ function registerFileRoutes(api, adapter) {
3027
3116
  if (existsSync3(newAbs)) {
3028
3117
  return c.json({ error: "already exists" }, 409);
3029
3118
  }
3119
+ const isDirectory = statSync3(res.absPath, { throwIfNoEntry: false })?.isDirectory() ?? false;
3030
3120
  ensureDir(res.project.dir, newAbs);
3031
3121
  renameSync2(res.absPath, newAbs);
3032
- const updatedFiles = updateReferences(res.project.dir, res.filePath, body.newPath);
3122
+ const updatedFiles = updateReferences(res.project.dir, res.filePath, body.newPath, isDirectory);
3033
3123
  return c.json({ ok: true, path: body.newPath, updatedReferences: updatedFiles });
3034
3124
  });
3035
3125
  api.post("/projects/:id/duplicate-file", async (c) => {
@@ -3095,17 +3185,18 @@ function registerFileRoutes(api, adapter) {
3095
3185
  });
3096
3186
  if ("error" in res) return res.error;
3097
3187
  const html = readFileSync3(res.absPath, "utf-8");
3188
+ const etag = `${fileContentVersion(html).slice(0, -1)}:gsap-animations:v1"`;
3189
+ const headers = { ETag: etag, "Cache-Control": "no-cache" };
3190
+ if (c.req.header("If-None-Match") === etag) return new Response(null, { status: 304, headers });
3098
3191
  const block = extractGsapScriptBlock(html);
3099
3192
  if (!block) {
3100
- return c.json({
3101
- animations: [],
3102
- timelineVar: "tl",
3103
- preamble: "",
3104
- postamble: ""
3105
- });
3193
+ return c.json(
3194
+ { animations: [], timelineVar: "tl", preamble: "", postamble: "" },
3195
+ 200,
3196
+ headers
3197
+ );
3106
3198
  }
3107
- const parsed = parseGsapScriptAcorn(block.scriptText);
3108
- return c.json(parsed);
3199
+ return c.json(parseGsapScriptAcorn(block.scriptText), 200, headers);
3109
3200
  });
3110
3201
  api.get("/projects/:id/gsap-mutation-capabilities", async (c) => {
3111
3202
  const project = await adapter.resolveProject(c.req.param("id"));
@@ -3282,6 +3373,9 @@ function tagRootCompositionFile(bodyHtml, compPath) {
3282
3373
  if (bodyHtml.slice(match.index, tagEnd).includes("data-composition-file")) return bodyHtml;
3283
3374
  return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
3284
3375
  }
3376
+ function rootHeadContent(rootHtml) {
3377
+ return rootHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i)?.[1] ?? "";
3378
+ }
3285
3379
  function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
3286
3380
  const compFile = join6(projectDir, compPath);
3287
3381
  if (!existsSync4(compFile)) return null;
@@ -3317,11 +3411,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
3317
3411
  rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
3318
3412
  const indexPath = join6(projectDir, "index.html");
3319
3413
  let headContent = "";
3320
- if (existsSync4(indexPath)) {
3321
- const indexHtml = readFileSync4(indexPath, "utf-8");
3322
- const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
3323
- headContent = headMatch?.[1] ?? "";
3324
- }
3414
+ if (existsSync4(indexPath)) headContent = rootHeadContent(readFileSync4(indexPath, "utf-8"));
3325
3415
  if (baseHref && !hasBaseElement(headContent)) {
3326
3416
  headContent = `<base href="${baseHref}">
3327
3417
  ${headContent}`;
@@ -4477,10 +4567,11 @@ import {
4477
4567
  writeFileSync as writeFileSync4
4478
4568
  } from "fs";
4479
4569
  import { join as join11 } from "path";
4480
- import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
4570
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
4481
4571
 
4482
4572
  // src/helpers/compositionInputs.ts
4483
4573
  import { readFileSync as readFileSync8 } from "fs";
4574
+ import { createHash as createHash3 } from "crypto";
4484
4575
  var ROOT_COMPOSITION = "index.html";
4485
4576
  var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
4486
4577
  function projectReader(projectDir) {
@@ -4517,27 +4608,51 @@ function closureOf(read, compPath) {
4517
4608
  return closure;
4518
4609
  }
4519
4610
  var inputSignatures = /* @__PURE__ */ new Map();
4611
+ var rootMemos = /* @__PURE__ */ new Map();
4612
+ var sha1 = (text2) => createHash3("sha1").update(text2).digest("hex");
4613
+ function rootHashes(read) {
4614
+ const source = read(ROOT_COMPOSITION) ?? "";
4615
+ return { root: sha1(source), head: sha1(rootHeadContent(source)) };
4616
+ }
4520
4617
  function compositionInputSignature(projectDir, compPath, projectSignature) {
4521
4618
  const key = `${projectDir}\0${compPath}`;
4522
4619
  const known = inputSignatures.get(key);
4523
4620
  if (known?.projectSignature === projectSignature) return known.inputSignature;
4524
4621
  const read = projectReader(projectDir);
4525
- const inputs = closureOf(read, normalizeSource(compPath) ?? compPath).add(ROOT_COMPOSITION);
4526
- const siblings = [...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path));
4527
- const inputSignature = createProjectSignature(projectDir, new Set(siblings));
4622
+ const comp = normalizeSource(compPath) ?? compPath;
4623
+ const inputs = closureOf(read, comp).add(ROOT_COMPOSITION);
4624
+ const excluded = new Set(
4625
+ [...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path))
4626
+ );
4627
+ let inputSignature;
4628
+ if (comp === ROOT_COMPOSITION) {
4629
+ inputSignature = createProjectSignature(projectDir, excluded);
4630
+ } else {
4631
+ const hashes = rootHashes(read);
4632
+ if (!rootMemos.has(projectDir)) rootMemos.set(projectDir, { ...hashes, affected: null });
4633
+ inputSignature = `${createProjectSignature(projectDir, excluded.add(ROOT_COMPOSITION))}:${hashes.head}`;
4634
+ }
4528
4635
  inputSignatures.set(key, { projectSignature, inputSignature });
4529
4636
  return inputSignature;
4530
4637
  }
4531
4638
  function compositionsAffectedBy(projectDir, changedPath) {
4532
4639
  const changed = changedPath.replace(/\\/g, "/");
4533
4640
  const read = projectReader(projectDir);
4641
+ if (changed === ROOT_COMPOSITION) {
4642
+ const now = rootHashes(read);
4643
+ const memo = rootMemos.get(projectDir);
4644
+ if (memo?.root === now.root) return memo.affected;
4645
+ const affected = memo?.head === now.head ? [ROOT_COMPOSITION] : null;
4646
+ rootMemos.set(projectDir, { ...now, affected });
4647
+ return affected;
4648
+ }
4534
4649
  const mounted = closureOf(read, ROOT_COMPOSITION);
4535
- if (changed === ROOT_COMPOSITION || !mounted.has(changed)) return null;
4650
+ if (!mounted.has(changed)) return null;
4536
4651
  return [...mounted].filter((path) => closureOf(read, path).has(changed));
4537
4652
  }
4538
4653
 
4539
4654
  // src/routes/thumbnail.ts
4540
- var THUMBNAIL_CACHE_VERSION = "v4";
4655
+ var THUMBNAIL_CACHE_VERSION = "v5";
4541
4656
  var THUMBNAIL_MAX_OUTPUT_WIDTH = 240;
4542
4657
  var THUMBNAIL_MAX_OUTPUT_HEIGHT = 135;
4543
4658
  var THUMBNAIL_CACHE_MAX_BYTES = 512 * 1024 * 1024;
@@ -4610,7 +4725,7 @@ function readFileOnce(file) {
4610
4725
  function manifestKey(file) {
4611
4726
  const manifest = readFileOnce(file);
4612
4727
  if (typeof manifest === "string") return { key: "", mtimeMs: -Infinity };
4613
- const hash = createHash3("sha1").update(manifest.data).digest("hex").slice(0, 16);
4728
+ const hash = createHash4("sha1").update(manifest.data).digest("hex").slice(0, 16);
4614
4729
  return { key: `_${hash}`, mtimeMs: Math.round(manifest.mtimeMs) };
4615
4730
  }
4616
4731
  function registerThumbnailRoutes(api, adapter) {
@@ -4647,7 +4762,7 @@ function registerThumbnailRoutes(api, adapter) {
4647
4762
  let sourceKey = "";
4648
4763
  if (source !== "missing") {
4649
4764
  const html = source.data.toString("utf-8");
4650
- sourceKey = `_${createHash3("sha1").update(html).digest("hex").slice(0, 16)}`;
4765
+ sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
4651
4766
  sourceMtime = Math.round(source.mtimeMs);
4652
4767
  if (!vpWidth) {
4653
4768
  const wMatch = html.match(/data-width=["'](\d+)["']/);
@@ -4663,9 +4778,9 @@ function registerThumbnailRoutes(api, adapter) {
4663
4778
  const previewPath = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
4664
4779
  const previewUrl = `${previewPath}?${PREVIEW_CAPTURE_PARAM}=1`;
4665
4780
  const cacheDir = join11(project.dir, ".thumbnails");
4666
- const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
4781
+ const selectorKey = selector ? `_${createHash4("sha1").update(selector).digest("hex").slice(0, 16)}_${selectorIndex ?? 0}` : "";
4667
4782
  const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
4668
- const inputSignatureKey = `_${createHash3("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
4783
+ const inputSignatureKey = `_${createHash4("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
4669
4784
  const outputScale = outputMode === "source" ? 1 : Math.min(1, THUMBNAIL_MAX_OUTPUT_WIDTH / compW, THUMBNAIL_MAX_OUTPUT_HEIGHT / compH);
4670
4785
  const outputWidth = Math.max(1, Math.round(compW * outputScale));
4671
4786
  const outputHeight = Math.max(1, Math.round(compH * outputScale));
@@ -4785,7 +4900,7 @@ import { findFfBinary as findFfBinary3 } from "@hyperframes/parsers/ff-binaries"
4785
4900
  import { replaceFileAtomically as replaceFileAtomically3 } from "@hyperframes/core/atomic-file";
4786
4901
 
4787
4902
  // src/helpers/freezeFrame.ts
4788
- import { createHash as createHash4, randomBytes } from "crypto";
4903
+ import { createHash as createHash5, randomBytes } from "crypto";
4789
4904
  import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
4790
4905
  import { readMediaOffsetSeconds } from "@hyperframes/parsers/media-duration";
4791
4906
  import { resolveRateSpec, sourceTimeAt } from "@hyperframes/core/speed-ramp";
@@ -4812,7 +4927,7 @@ function freezeExtractArgs(src, mediaTime, output) {
4812
4927
  var randomStillToken = () => randomBytes(4).toString("hex");
4813
4928
  function freezeStillFileName(clipId, playhead, token = randomStillToken()) {
4814
4929
  const stem = clipId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 48) || "clip";
4815
- const idHash = createHash4("sha256").update(clipId).digest("hex").slice(0, 10);
4930
+ const idHash = createHash5("sha256").update(clipId).digest("hex").slice(0, 10);
4816
4931
  return `${stem}-${idHash}-${Math.round(playhead * 1e3)}-${token}.png`;
4817
4932
  }
4818
4933
  function readFreezeSource(html, target, playhead) {
@@ -6020,13 +6135,13 @@ function numericAttribute(element, name) {
6020
6135
  }
6021
6136
 
6022
6137
  // src/helpers/previewDocumentStore.ts
6023
- import { createHash as createHash5 } from "crypto";
6138
+ import { createHash as createHash6 } from "crypto";
6024
6139
  import { readFileSync as readFileSync14, readdirSync as readdirSync5, rmSync as rmSync5 } from "fs";
6025
6140
  import { join as join17 } from "path";
6026
6141
  import { replaceFileAtomically as replaceFileAtomically4 } from "@hyperframes/core/atomic-file";
6027
6142
  function createPreviewDocumentStore(projectDir, salt) {
6028
6143
  const dir = join17(projectDir, ".hyperframes", "preview");
6029
- const fileFor = (key) => join17(dir, `${createHash5("sha256").update(`${salt}
6144
+ const fileFor = (key) => join17(dir, `${createHash6("sha256").update(`${salt}
6030
6145
  ${key}`).digest("hex").slice(0, 32)}.html`);
6031
6146
  return {
6032
6147
  read(key) {
@@ -6136,6 +6251,7 @@ export {
6136
6251
  applyFileMutations,
6137
6252
  buildSubCompositionHtml,
6138
6253
  cleanupProxyCache,
6254
+ clearElementScreenshotIsolation,
6139
6255
  compositionsAffectedBy,
6140
6256
  consumeFileWriteReceipt,
6141
6257
  createBackgroundRemovalJob,