@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.
- package/dist/{chunk-EPL4YKGI.js → chunk-E3HLZKPI.js} +16 -7
- package/dist/chunk-E3HLZKPI.js.map +1 -0
- package/dist/{chunk-Z4DIWF3Z.js → chunk-G4YB7Q2Q.js} +24 -9
- package/dist/chunk-G4YB7Q2Q.js.map +1 -0
- package/dist/{chunk-W2SBTCO2.js → chunk-IMG2GSY6.js} +21 -2
- package/dist/chunk-IMG2GSY6.js.map +1 -0
- package/dist/helpers/screenshotClip.d.ts +2 -1
- package/dist/helpers/screenshotClip.js +3 -1
- package/dist/helpers/sourceMutation.d.ts +8 -1
- package/dist/helpers/sourceMutation.js +5 -1
- package/dist/history/index.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +156 -40
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-EPL4YKGI.js.map +0 -1
- package/dist/chunk-W2SBTCO2.js.map +0 -1
- package/dist/chunk-Z4DIWF3Z.js.map +0 -1
|
@@ -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 };
|
|
@@ -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-
|
|
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
|
package/dist/history/index.js
CHANGED
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:
|
|
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-
|
|
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-
|
|
49
|
+
} from "./chunk-E3HLZKPI.js";
|
|
48
50
|
import {
|
|
51
|
+
clearElementScreenshotIsolation,
|
|
49
52
|
getElementScreenshotClip
|
|
50
|
-
} from "./chunk-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
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
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
});
|
|
3193
|
+
return c.json(
|
|
3194
|
+
{ animations: [], timelineVar: "tl", preamble: "", postamble: "" },
|
|
3195
|
+
200,
|
|
3196
|
+
headers
|
|
3197
|
+
);
|
|
3106
3198
|
}
|
|
3107
|
-
|
|
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
|
|
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
|
|
4526
|
-
const
|
|
4527
|
-
const
|
|
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 (
|
|
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 = "
|
|
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 =
|
|
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 = `_${
|
|
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.
|
|
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 = `_${
|
|
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
|
|
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 =
|
|
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
|
|
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, `${
|
|
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,
|