@hyperframes/studio-server 0.8.29 → 0.8.31
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-KMXV2QLX.js → chunk-AG4UCNIY.js} +2 -2
- package/dist/chunk-AG4UCNIY.js.map +1 -0
- package/dist/{chunk-KW5URJHI.js → chunk-C6CSSUAY.js} +1 -1
- package/dist/{chunk-KW5URJHI.js.map → chunk-C6CSSUAY.js.map} +1 -1
- package/dist/helpers/mediaProxyPreview.js +1 -1
- package/dist/helpers/sourceMutation.js +1 -1
- package/dist/index.js +113 -66
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-KMXV2QLX.js.map +0 -1
|
@@ -349,7 +349,7 @@ function setInlineLeftTop(el, left, top) {
|
|
|
349
349
|
el.setAttribute("style", style);
|
|
350
350
|
}
|
|
351
351
|
function uniqueGroupDomId(document, groupId) {
|
|
352
|
-
const base = groupId.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(
|
|
352
|
+
const base = groupId.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "group";
|
|
353
353
|
let id = base;
|
|
354
354
|
let n = 2;
|
|
355
355
|
while (document.getElementById(id)) {
|
|
@@ -465,4 +465,4 @@ export {
|
|
|
465
465
|
wrapElementsInHtml,
|
|
466
466
|
unwrapElementsFromHtml
|
|
467
467
|
};
|
|
468
|
-
//# sourceMappingURL=chunk-
|
|
468
|
+
//# sourceMappingURL=chunk-AG4UCNIY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/helpers/sourceMutation.ts","../src/helpers/sourceStyleMutation.ts"],"sourcesContent":["import { parseHTML } from \"linkedom\";\nimport { removeElementWithGsapCascade } from \"@hyperframes/parsers\";\nimport postcss from \"postcss\";\nimport selectorParser from \"postcss-selector-parser\";\nimport { isAllowedHtmlAttribute, isSafeAttributeValue } from \"@hyperframes/core/html-attr-safety\";\nimport { sanitizeRichTextChildren } from \"@hyperframes/core/rich-text-sanitize\";\nimport {\n EXCLUDED_TAGS,\n ensureHfIds,\n mintHfId,\n walkCompositionDescendants,\n} from \"@hyperframes/parsers/hf-ids\";\nimport { readClipTiming, writeClipTiming } from \"@hyperframes/core/composition-contract\";\nimport { parseStyleDecls, patchStyleAttrString } from \"./sourceStyleMutation.js\";\n\nexport interface SourceMutationTarget {\n id?: string | null;\n hfId?: string;\n selector?: string;\n selectorIndex?: number;\n}\n\nfunction parseSourceDocument(source: string): { document: Document; wrappedFragment: boolean } {\n const hasDocumentShell = /<!doctype|<html[\\s>]/i.test(source);\n if (hasDocumentShell) {\n return { document: parseHTML(source).document, wrappedFragment: false };\n }\n return {\n document: parseHTML(`<!DOCTYPE html><html><head></head><body>${source}</body></html>`).document,\n wrappedFragment: true,\n };\n}\n\nfunction duplicateCssRulesForId(document: Document, originalId: string, newId: string): void {\n const idToken = `#${originalId}`;\n const transform = selectorParser((selectors) => {\n selectors.walkIds((node) => {\n if (node.value === originalId) node.value = newId;\n });\n });\n for (const styleEl of document.querySelectorAll(\"style\")) {\n const css = styleEl.textContent ?? \"\";\n let root: postcss.Root;\n try {\n root = postcss.parse(css);\n } catch {\n continue;\n }\n const clones: postcss.Rule[] = [];\n root.walkRules((rule) => {\n if (!rule.selector.includes(idToken)) return;\n const newSelector = transform.processSync(rule.selector);\n if (newSelector === rule.selector) return;\n const clone = rule.clone({ selector: newSelector });\n clones.push(clone);\n });\n if (clones.length > 0) {\n for (const c of clones) root.append(c);\n styleEl.textContent = root.toString();\n }\n }\n}\n\nfunction querySelectorAllWithTemplates(root: Document | Element, selector: string): Element[] {\n const matches = Array.from(root.querySelectorAll(selector));\n if (matches.length > 0) return matches;\n // querySelectorAll doesn't traverse <template> content in linkedom.\n // Search directly on each template element (NOT .content — removing from\n // .content's DocumentFragment doesn't update the serialized output).\n // Recurse so NESTED templates resolve too — ensureHfIds and the SDK's\n // querySelectorAllDeep descend nested composition templates, so ids exist at\n // any template depth; a one-level search here would silently no-op\n // server-side ops on those ids while the SDK resolves them.\n const templates = Array.from(root.querySelectorAll(\"template\"));\n for (const tmpl of templates) {\n const inner = querySelectorAllWithTemplates(tmpl, selector);\n if (inner.length > 0) return inner;\n }\n return [];\n}\n\n// Prevent CSS attribute-selector injection via a crafted hfId: escape\n// backslashes first, then double-quotes. Keeps a malformed/hostile value from\n// breaking out of the `[data-hf-id=\"…\"]` selector once callers beyond the\n// internal mint contract (R2+ user flows) pass values here.\nfunction escapeCssAttrValue(value: string): string {\n return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\nfunction findByHfId(document: Document, hfId: string): Element | null {\n try {\n const matches = querySelectorAllWithTemplates(\n document,\n `[data-hf-id=\"${escapeCssAttrValue(hfId)}\"]`,\n );\n if (matches.length > 1) {\n // The mint contract guarantees uniqueness; a duplicate means upstream\n // id drift. Don't silently patch an arbitrary one — surface it.\n // eslint-disable-next-line no-console\n console.warn(\n `sourceMutation: data-hf-id \"${hfId}\" matched ${matches.length} elements; using the first. ids must be unique per document.`,\n );\n }\n return matches[0] ?? null;\n } catch {\n // Malformed selector despite escaping — let the caller fall back.\n return null;\n }\n}\n\nfunction findTargetElement(document: Document, target: SourceMutationTarget): Element | null {\n if (target.hfId) {\n const el = findByHfId(document, target.hfId);\n if (el) return el;\n }\n\n if (target.id) {\n const byId = document.getElementById(target.id);\n if (byId) return byId;\n }\n\n if (!target.selector) return null;\n try {\n const matches = querySelectorAllWithTemplates(document, target.selector);\n return matches[target.selectorIndex ?? 0] ?? null;\n } catch {\n return null;\n }\n}\n\nexport function removeElementFromHtml(source: string, target: SourceMutationTarget): string {\n const { document, wrappedFragment } = parseSourceDocument(source);\n const element = findTargetElement(document, target);\n if (!element) return source;\n\n removeElementWithGsapCascade(document, element);\n return wrappedFragment ? document.body.innerHTML || \"\" : document.toString();\n}\n\nexport function isHTMLElement(el: Node): el is HTMLElement {\n const HTMLEl = el.ownerDocument?.defaultView?.HTMLElement;\n return HTMLEl ? el instanceof HTMLEl : el.nodeType === 1 && \"style\" in el;\n}\n\nexport interface PatchOperation {\n type: \"inline-style\" | \"attribute\" | \"html-attribute\" | \"text-content\" | \"rich-text\";\n property: string;\n value: string | null;\n childSelector?: string;\n childIndex?: number;\n}\n\ninterface ResolvedPatchOperation {\n op: PatchOperation;\n target: HTMLElement;\n}\n\nfunction resolveOperationTarget(parent: HTMLElement, op: PatchOperation): HTMLElement | null {\n if (op.childSelector === undefined) return parent;\n try {\n const child = parent.querySelectorAll(op.childSelector)[op.childIndex ?? 0] ?? null;\n return child && isHTMLElement(child) ? child : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Give the elements a rich-text patch just introduced their stable ids, here,\n * in the bytes about to be written and handed back.\n *\n * Otherwise the next preview request mints them and writes the file a second\n * time, after Studio has already recorded the edit in its history. The recorded\n * \"after\" stops matching disk, the content check refuses, and undo reports the\n * file as changed outside Studio — for every colour applied to a run of\n * characters and every text layer added. The clip split stamps its own clone\n * for exactly this reason.\n *\n * Minted one element at a time with the same function `ensureHfIds` uses, so\n * these ids are the ones the next pass would have assigned. Not `ensureHfIds`\n * itself: it takes a whole document, and handing it this element's markup would\n * put the markup back as one.\n */\nfunction stampNewChildIds(parent: Element): void {\n const assigned = new Set<string>();\n const root = parent.ownerDocument?.body ?? parent;\n walkCompositionDescendants(root, (el) => {\n const id = el.getAttribute(\"data-hf-id\");\n if (id) assigned.add(id);\n });\n for (const el of parent.querySelectorAll(\"*\")) {\n if (el.getAttribute(\"data-hf-id\")) continue;\n if (EXCLUDED_TAGS.has(el.tagName.toLowerCase())) continue;\n el.setAttribute(\"data-hf-id\", mintHfId(el, assigned));\n }\n}\n\n// fallow-ignore-next-line complexity\nexport function patchElementInHtml(\n source: string,\n target: SourceMutationTarget,\n operations: PatchOperation[],\n): { html: string; matched: boolean } {\n const { document, wrappedFragment } = parseSourceDocument(source);\n const el = findTargetElement(document, target);\n if (!el || !isHTMLElement(el)) return { html: source, matched: false };\n const htmlEl = el;\n\n const resolved: ResolvedPatchOperation[] = [];\n for (const op of operations) {\n const opTarget = resolveOperationTarget(htmlEl, op);\n if (!opTarget) return { html: source, matched: false };\n resolved.push({ op, target: opTarget });\n }\n\n for (const { op, target: opTarget } of resolved) {\n switch (op.type) {\n case \"inline-style\":\n // linkedom's CSSStyleDeclaration does not support CSS custom properties\n // (--foo) or newer individual transform properties (translate, rotate,\n // scale) via style.setProperty(). Manipulate the style attribute string\n // directly so all property names survive the round-trip.\n {\n const raw = opTarget.getAttribute(\"style\") ?? \"\";\n const patched = patchStyleAttrString(raw, op.property, op.value);\n opTarget.setAttribute(\"style\", patched);\n }\n break;\n case \"attribute\":\n {\n const fullAttr = op.property.startsWith(\"data-\") ? op.property : `data-${op.property}`;\n if (op.value != null) {\n opTarget.setAttribute(fullAttr, op.value);\n } else {\n opTarget.removeAttribute(fullAttr);\n }\n }\n break;\n case \"html-attribute\":\n if (!isAllowedHtmlAttribute(op.property)) break;\n if (op.value != null) {\n if (!isSafeAttributeValue(op.property, op.value)) break;\n opTarget.setAttribute(op.property, op.value);\n } else {\n opTarget.removeAttribute(op.property);\n }\n break;\n case \"text-content\":\n if (op.value != null) {\n const inner = opTarget.children.length === 1 ? opTarget.firstElementChild : null;\n const textTarget = inner && isHTMLElement(inner) ? inner : opTarget;\n textTarget.textContent = op.value;\n }\n break;\n // The one operation that can write markup, so the one that has to check\n // it. Assigned first and sanitised after, rather than sanitising a\n // string: parsing is what turns a payload into the tree the allowlist\n // can actually judge, and linkedom never runs anything it parses.\n case \"rich-text\":\n if (op.value != null) {\n opTarget.innerHTML = op.value;\n sanitizeRichTextChildren(opTarget);\n stampNewChildIds(opTarget);\n }\n break;\n }\n }\n\n return {\n html: wrappedFragment ? document.body.innerHTML || \"\" : document.toString(),\n matched: true,\n };\n}\n\nexport function probeElementInSource(source: string, target: SourceMutationTarget): boolean {\n if (!target.id && !target.hfId && !target.selector) return false;\n const { document } = parseSourceDocument(source);\n const el = findTargetElement(document, target);\n return el != null && isHTMLElement(el);\n}\n\nexport interface SplitElementResult {\n html: string;\n matched: boolean;\n newId: string | null;\n}\n\nfunction resolveElementTiming(el: Element): {\n start: number;\n duration: number;\n} {\n const timing = readClipTiming(el);\n return { start: timing.start ?? 0, duration: timing.duration ?? 0 };\n}\n\nfunction setElementDuration(el: Element, start: number, duration: number): void {\n writeClipTiming(el, {\n start: Math.round(start * 1000) / 1000,\n duration: Math.round(duration * 1000) / 1000,\n });\n}\n\n// fallow-ignore-next-line complexity\nexport function splitElementInHtml(\n source: string,\n target: SourceMutationTarget,\n splitTime: number,\n newId: string,\n fallbackTiming?: {\n start: number;\n duration: number;\n playbackStart?: number;\n playbackRate?: number;\n stampPlaybackStart?: boolean;\n },\n): SplitElementResult {\n const { document, wrappedFragment } = parseSourceDocument(source);\n const el = findTargetElement(document, target);\n if (!el || !isHTMLElement(el)) return { html: source, matched: false, newId: null };\n\n const timing = resolveElementTiming(el);\n let { start, duration } = timing;\n // GSAP-animated elements carry their timing in the script, not in data-* attrs,\n // so the source has no authored duration. Fall back to the store's (GSAP-derived)\n // range — the runtime windows visibility off data-start/data-duration regardless\n // of class, so stamping both halves below makes each half show only in its window.\n if (duration <= 0 && fallbackTiming && fallbackTiming.duration > 0) {\n start = fallbackTiming.start;\n duration = fallbackTiming.duration;\n }\n if (duration <= 0 || splitTime <= start || splitTime >= start + duration) {\n return { html: source, matched: false, newId: null };\n }\n\n if (document.getElementById(newId)) {\n let suffix = 2;\n const base = newId;\n while (document.getElementById(newId)) {\n newId = `${base}-${suffix++}`;\n }\n }\n\n const firstDuration = splitTime - start;\n const secondDuration = duration - firstDuration;\n\n const clone = el.cloneNode(true);\n if (!isHTMLElement(clone)) return { html: source, matched: false, newId: null };\n clone.setAttribute(\"id\", newId);\n const compositionId = clone.getAttribute(\"data-composition-id\");\n if (compositionId) {\n const usedCompositionIds = new Set(\n Array.from(document.querySelectorAll(\"[data-composition-id]\"), (node) =>\n node.getAttribute(\"data-composition-id\"),\n ),\n );\n const base = `${compositionId}-split`;\n let nextCompositionId = base;\n let suffix = 2;\n while (usedCompositionIds.has(nextCompositionId)) nextCompositionId = `${base}-${suffix++}`;\n clone.setAttribute(\"data-composition-id\", nextCompositionId);\n }\n clone.removeAttribute(\"data-hf-id\");\n // Descendants carry their own data-hf-id; leaving them duplicates the id of\n // every nested node (e.g. an inner <span>), so strip them on the clone too.\n for (const node of clone.querySelectorAll(\"[data-hf-id]\")) node.removeAttribute(\"data-hf-id\");\n setElementDuration(clone, splitTime, secondDuration);\n\n // Keep the \"clip\" class — the runtime uses it to control visibility\n // based on data-start/data-duration timing.\n\n // A split creates two views over the same media source. Even an untrimmed\n // audio/video element needs an explicit zero in-point stamped on the first\n // half so the second half can advance from it instead of restarting at zero.\n const playbackStartAttr = el.hasAttribute(\"data-playback-start\")\n ? \"data-playback-start\"\n : el.hasAttribute(\"data-media-start\")\n ? \"data-media-start\"\n : fallbackTiming?.stampPlaybackStart\n ? \"data-playback-start\"\n : el.matches(\"audio, video\")\n ? \"data-media-start\"\n : null;\n if (playbackStartAttr) {\n const currentTrim =\n parseFloat(el.getAttribute(playbackStartAttr) ?? \"\") || fallbackTiming?.playbackStart || 0;\n const rateRaw = parseFloat(el.getAttribute(\"data-playback-rate\") ?? \"\");\n const rate =\n Number.isFinite(rateRaw) && rateRaw > 0 ? rateRaw : (fallbackTiming?.playbackRate ?? 1);\n el.setAttribute(playbackStartAttr, String(Math.round(currentTrim * 1000) / 1000));\n clone.setAttribute(\n playbackStartAttr,\n String(Math.round((currentTrim + firstDuration * rate) * 1000) / 1000),\n );\n }\n\n // Duplicate CSS rules targeting the original ID so the clone inherits the same styles.\n const originalId = el.getAttribute(\"id\");\n if (originalId) {\n duplicateCssRulesForId(document, originalId, newId);\n }\n\n // Trim the original element's duration. A GSAP element had no data-start; stamp\n // it so the runtime windows the first half (visibility selects on [data-start]).\n setElementDuration(el, start, firstDuration);\n\n // Insert clone after original\n if (el.nextSibling) {\n el.parentElement!.insertBefore(clone, el.nextSibling);\n } else {\n el.parentElement!.appendChild(clone);\n }\n\n const html = wrappedFragment ? document.body.innerHTML || \"\" : document.toString();\n return {\n // The split owns its new nodes' stable ids. Leaving the clone unstamped makes\n // the next preview request persist different bytes after history is recorded.\n html: ensureHfIds(html),\n matched: true,\n newId,\n };\n}\n\n// --- Element grouping -------------------------------------------------------\n// A group is a real `<div data-hf-group=\"…\">` wrapping its members in the DOM.\n// Wrapping rebases each member's left/top so its absolute position is unchanged:\n// the wrapper sits at the selection bbox top-left, and each child's new left/top\n// is its old left/top minus the wrapper origin (computed client-side, where live\n// layout is available, and passed in via `rebases`). GSAP x/y, CSS translate and\n// --hf-studio-offset vars are deltas relative to flow position and stay untouched.\n\nexport interface WrapElementsResult {\n html: string;\n matched: boolean;\n groupId: string | null;\n error?: string;\n}\n\nexport interface UnwrapElementsResult {\n html: string;\n unwrapped: boolean;\n /** The unwrapped wrapper's id, so callers can strip GSAP that targeted it\n * (the wrapper is gone; a leftover `gsap.set(\"#id\")` would throw at runtime). */\n unwrappedGroupId?: string;\n /** Members (id'd children) with their absolute layout centres (post un-rebase),\n * so the caller can BAKE the group's GSAP transform into each member before\n * stripping it — otherwise the group's moves are lost on ungroup. */\n members?: Array<{ id: string; cx: number; cy: number }>;\n /** The wrapper's layout centre — the pivot for baking the group's rotation/scale. */\n groupCenter?: { cx: number; cy: number };\n}\n\nexport interface ElementRebase {\n target: SourceMutationTarget;\n left: number;\n top: number;\n}\n\nfunction getInlineStylePx(el: Element, property: string): number {\n const style = (isHTMLElement(el) ? el.getAttribute(\"style\") : null) ?? \"\";\n const { props } = parseStyleDecls(style);\n const raw = props.get(property);\n if (!raw) return 0;\n const n = parseFloat(raw);\n return Number.isFinite(n) ? n : 0;\n}\n\nfunction setInlineLeftTop(el: HTMLElement, left: number, top: number): void {\n let style = el.getAttribute(\"style\") ?? \"\";\n style = patchStyleAttrString(style, \"left\", `${left}px`);\n style = patchStyleAttrString(style, \"top\", `${top}px`);\n el.setAttribute(\"style\", style);\n}\n\n// Slug the group name (\"Group 1\" → \"group-1\") into a unique, valid element id.\nfunction uniqueGroupDomId(document: Document, groupId: string): string {\n const base =\n groupId\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n // Normalization above leaves at most one hyphen at either edge.\n .replace(/^-|-$/g, \"\") || \"group\";\n let id = base;\n let n = 2;\n while (document.getElementById(id)) {\n id = `${base}-${n}`;\n n += 1;\n }\n return id;\n}\n\n// fallow-ignore-next-line complexity\nexport function wrapElementsInHtml(\n source: string,\n targets: SourceMutationTarget[],\n groupId: string,\n bbox: { left: number; top: number; width: number; height: number },\n rebases: ElementRebase[],\n): WrapElementsResult {\n const { document, wrappedFragment } = parseSourceDocument(source);\n if (targets.length === 0) {\n return { html: source, matched: false, groupId: null, error: \"no targets\" };\n }\n\n // Resolve + dedupe by element ref (two targets may point at the same node).\n const els: HTMLElement[] = [];\n const seen = new Set<Element>();\n for (const target of targets) {\n const el = findTargetElement(document, target);\n if (!el || !isHTMLElement(el) || seen.has(el)) continue;\n seen.add(el);\n els.push(el);\n }\n if (els.length === 0) {\n return { html: source, matched: false, groupId: null, error: \"no targets matched\" };\n }\n\n // P1: require a single common parent (LCA multi-parent wrapping is P2).\n const parent = els[0]?.parentElement;\n if (!parent || els.some((el) => el.parentElement !== parent)) {\n return {\n html: source,\n matched: false,\n groupId: null,\n error: \"grouped elements must share a single parent\",\n };\n }\n\n // Order members by their position in the parent (= z-order / stacking order).\n const memberSet = new Set<Element>(els);\n const ordered = Array.from(parent.children).filter((c): c is HTMLElement => memberSet.has(c));\n\n // Map each member to its rebased left/top (resolved against the same document).\n const rebaseByEl = new Map<Element, { left: number; top: number }>();\n for (const rebase of rebases) {\n const el = findTargetElement(document, rebase.target);\n if (el) rebaseByEl.set(el, { left: rebase.left, top: rebase.top });\n }\n\n const wrapper = document.createElement(\"div\");\n wrapper.setAttribute(\"data-hf-group\", groupId);\n // A real `id` (slug of the group name) makes the wrapper a first-class node in the\n // clip manifest / timeline parent-map (both keyed by id) and a clean GSAP target —\n // without it the wrapper is invisible to the timeline and breaks child enumeration.\n wrapper.setAttribute(\"id\", uniqueGroupDomId(document, groupId));\n // Adopt the topmost member's stacking level. A group is one stacking unit, so a\n // non-member interleaved between two selected members can't stay \"between\" them\n // once they unify. Matching Figma/Sketch, the group lifts to the topmost selected\n // layer: the wrapper goes at the LAST member's slot and carries the max member\n // z-index — so an interleaved non-member falls below the group instead of hoisting\n // above it, and explicit member z-indexes are honored.\n const memberZIndexes = ordered\n .map((el) =>\n Number.parseInt(\n parseStyleDecls(el.getAttribute(\"style\") ?? \"\").props.get(\"z-index\") ?? \"\",\n 10,\n ),\n )\n .filter((z) => Number.isFinite(z));\n const maxZ = memberZIndexes.length > 0 ? Math.max(...memberZIndexes) : null;\n wrapper.setAttribute(\n \"style\",\n `position: absolute; left: ${bbox.left}px; top: ${bbox.top}px; width: ${bbox.width}px; height: ${bbox.height}px` +\n (maxZ !== null ? `; z-index: ${maxZ}` : \"\"),\n );\n\n // Insert the wrapper at the topmost member's slot, then move members into it.\n parent.insertBefore(wrapper, ordered[ordered.length - 1] ?? null);\n for (const el of ordered) {\n const rebase = rebaseByEl.get(el);\n if (rebase) setInlineLeftTop(el, rebase.left, rebase.top);\n wrapper.appendChild(el); // appendChild moves the node, preserving order\n }\n\n return {\n html: wrappedFragment ? document.body.innerHTML || \"\" : document.toString(),\n matched: true,\n groupId,\n };\n}\n\nexport function unwrapElementsFromHtml(\n source: string,\n groupTarget: SourceMutationTarget,\n): UnwrapElementsResult {\n const { document, wrappedFragment } = parseSourceDocument(source);\n const group = findTargetElement(document, groupTarget);\n if (!group || !isHTMLElement(group)) return { html: source, unwrapped: false };\n // Shape guard mirroring the wrap-side contract: only ever dissolve an actual\n // group wrapper. A stale/desynced selection that resolves to a plain <div>\n // would otherwise be unwrapped — rebasing its children by the parent's origin\n // (silent corruption). Wrap enforces invariants; unwrap must too.\n if (!group.hasAttribute(\"data-hf-group\")) return { html: source, unwrapped: false };\n\n const parent = group.parentElement;\n if (!parent) return { html: source, unwrapped: false };\n\n // Undo the rebase: child absolute position = child (rebased) + wrapper origin.\n const wLeft = getInlineStylePx(group, \"left\");\n const wTop = getInlineStylePx(group, \"top\");\n const groupCenter = {\n cx: wLeft + getInlineStylePx(group, \"width\") / 2,\n cy: wTop + getInlineStylePx(group, \"height\") / 2,\n };\n\n // Move children back to the wrapper's slot, preserving order.\n const members: Array<{ id: string; cx: number; cy: number }> = [];\n for (const child of Array.from(group.children)) {\n if (isHTMLElement(child)) {\n const newLeft = getInlineStylePx(child, \"left\") + wLeft;\n const newTop = getInlineStylePx(child, \"top\") + wTop;\n setInlineLeftTop(child, newLeft, newTop);\n if (child.id) {\n members.push({\n id: child.id,\n cx: newLeft + getInlineStylePx(child, \"width\") / 2,\n cy: newTop + getInlineStylePx(child, \"height\") / 2,\n });\n }\n }\n parent.insertBefore(child, group);\n }\n const groupId = group.id || undefined;\n group.remove();\n\n return {\n html: wrappedFragment ? document.body.innerHTML || \"\" : document.toString(),\n unwrapped: true,\n unwrappedGroupId: groupId,\n members,\n groupCenter,\n };\n}\n","// fallow-ignore-next-line complexity\nexport function parseStyleDecls(style: string): { props: Map<string, string>; order: string[] } {\n const props = new Map<string, string>();\n const order: string[] = [];\n let i = 0;\n while (i < style.length) {\n let depth = 0;\n let inSingle = false;\n let inDouble = false;\n const start = i;\n while (i < style.length) {\n const ch = style[i];\n if (ch === \"'\" && !inDouble) inSingle = !inSingle;\n else if (ch === '\"' && !inSingle) inDouble = !inDouble;\n else if (!inSingle && !inDouble) {\n if (ch === \"(\") depth++;\n else if (ch === \")\") depth = Math.max(0, depth - 1);\n else if (ch === \";\" && depth === 0) break;\n }\n i++;\n }\n const decl = style.slice(start, i).trim();\n i++;\n if (!decl) continue;\n const colon = decl.indexOf(\":\");\n if (colon < 0) continue;\n const key = decl.slice(0, colon).trim();\n const val = decl.slice(colon + 1).trim();\n if (!key) continue;\n if (!props.has(key)) order.push(key);\n props.set(key, val);\n }\n return { props, order };\n}\n\nfunction serializeStyleDecls(props: Map<string, string>, order: string[]): string {\n return order\n .map((k) => `${k}: ${props.get(k) ?? \"\"}`)\n .filter((d) => d.trim())\n .join(\"; \");\n}\n\nexport function patchStyleAttrString(\n style: string,\n property: string,\n value: string | null,\n): string {\n const { props, order } = parseStyleDecls(style);\n if (value === null) {\n props.delete(property);\n const idx = order.indexOf(property);\n if (idx >= 0) order.splice(idx, 1);\n } else {\n if (!props.has(property)) order.push(property);\n props.set(property, value);\n }\n return serializeStyleDecls(props, order);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/helpers/mediaProxyPreview.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport type { StudioApiAdapter } from \"../types.js\";\nimport {\n createMediaCodecProbeCache,\n proxyVariantFor,\n scanProjectMediaCodecMap,\n type HtmlSourceLike,\n type MediaCodecMap,\n type MediaCodecProbeCache,\n} from \"./mediaCodecMap.js\";\nimport { resolveProxy, PROXY_PARAMS_VERSION } from \"./proxyTranscoder.js\";\n\n/**\n * Transparent-media-proxy wiring shared by `routes/preview.ts`\n * (docs/plans/2026-07-14-002-feat-transparent-media-proxies-plan.md, unit U3).\n * Split out of the route module to keep it under the repo's 600-line file cap.\n */\n\n/**\n * Preview-route-local adapter surface for the auto-proxy feature. Both\n * fields are optional so any existing `StudioApiAdapter` value remains\n * structurally assignable without editing the shared interface:\n * `autoProxy` defaults to true (on) when omitted — a later unit wires the\n * CLI `--no-proxy` flag / `hyperframes.json` setting through it;\n * `mediaCodecProbeCache` lets a host share one probe cache across\n * preview/play/static-server surfaces instead of each constructing its own.\n */\nexport type PreviewApiAdapter = StudioApiAdapter & {\n autoProxy?: boolean;\n mediaCodecProbeCache?: MediaCodecProbeCache;\n};\n\nexport function isAutoProxyEnabled(adapter: PreviewApiAdapter): boolean {\n return adapter.autoProxy !== false;\n}\n\n/** One probe cache per server instance — construct once in `registerPreviewRoutes`\n * and reuse across every request so the mtime-cache benefit in\n * `scanProjectMediaCodecMap` actually applies. A host that wants to share the\n * cache across other surfaces (play, static project server) can pass its own\n * via `adapter.mediaCodecProbeCache`. */\nexport function resolvePreviewMediaCodecProbeCache(\n adapter: PreviewApiAdapter,\n): MediaCodecProbeCache {\n return adapter.mediaCodecProbeCache ?? createMediaCodecProbeCache();\n}\n\n/**\n * ETag salt for `?hf-proxy=` asset requests, mirroring `variablesEtagSalt` in\n * preview.ts: salted by the raw param value plus the transcoder's params\n * version, so a future proxy-recipe change (which bumps `PROXY_PARAMS_VERSION`)\n * or a different proxy variant invalidates cached 304s without needing to\n * touch the proxy file itself.\n */\nexport function proxyEtagSalt(raw: string | undefined): string {\n if (raw === undefined) return \"\";\n return `:proxy:${raw}:${PROXY_PARAMS_VERSION}`;\n}\n\n// Mirrors `injectScriptTagIntoHead` in routes/preview.ts (kept local rather\n// than imported to avoid a helpers → routes dependency edge for one\n// two-line utility).\nfunction injectScriptTagIntoHead(html: string, scriptTag: string): string {\n if (html.includes(\"</head>\")) return html.replace(\"</head>\", `${scriptTag}\\n</head>`);\n return `${scriptTag}\\n${html}`;\n}\n\n/**\n * Injects `window.__HF_MEDIA_CODEC_MAP__` (the U1 codec-facts scan) into\n * served composition HTML, and fire-and-forget pre-warms `resolveProxy` for\n * every browser-hostile entry so an element's proactive swap usually hits a\n * warm cache (KTD: protects the per-origin connection budget under held\n * responses). No second concurrency limiter here — the transcoder's own\n * global bound throttles both pre-warm and element-triggered calls.\n * Pre-warm failures are swallowed; an actual `?hf-proxy=` request surfaces\n * them as a 502. Alpha-bearing entries pre-warm their VP8/WebM variant.\n *\n * The single shared implementation for every auto-proxy surface — the studio\n * preview route (via `injectMediaCodecMap` below) and the CLI's composition /\n * static project servers (via the `./media-proxy-preview` subpath export).\n * Empty maps leave HTML untouched, preserving the normal no-hostile-media\n * preview path. On-demand proxy requests enforce the same eligibility gate.\n */\nexport async function injectMediaCodecMapIntoHtml(\n html: string,\n projectDir: string,\n htmlSources: HtmlSourceLike[],\n probeCache?: MediaCodecProbeCache,\n): Promise<string> {\n let map: MediaCodecMap;\n try {\n map = await scanProjectMediaCodecMap(\n projectDir,\n htmlSources,\n probeCache ? { cache: probeCache } : {},\n );\n } catch {\n // Best-effort: a scan failure must never block serving the page.\n return html;\n }\n if (Object.keys(map).length === 0) return html;\n for (const [rootRelativePathname, facts] of Object.entries(map)) {\n if (!facts.browserHostile) continue;\n resolveProxy(\n projectDir,\n resolve(projectDir, rootRelativePathname.replace(/^\\/+/, \"\")),\n proxyVariantFor(facts),\n ).catch(() => {\n // Swallowed: the pre-warm is best-effort. A real `?hf-proxy=` request\n // for this asset re-attempts the transcode and reports failure (502).\n });\n }\n // <-escape prevents a src path containing \"</script>\" from breaking out of\n // the injected tag, mirroring injectPreviewVariables in
|
|
1
|
+
{"version":3,"sources":["../src/helpers/mediaProxyPreview.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport type { StudioApiAdapter } from \"../types.js\";\nimport {\n createMediaCodecProbeCache,\n proxyVariantFor,\n scanProjectMediaCodecMap,\n type HtmlSourceLike,\n type MediaCodecMap,\n type MediaCodecProbeCache,\n} from \"./mediaCodecMap.js\";\nimport { resolveProxy, PROXY_PARAMS_VERSION } from \"./proxyTranscoder.js\";\n\n/**\n * Transparent-media-proxy wiring shared by `routes/preview.ts`\n * (docs/plans/2026-07-14-002-feat-transparent-media-proxies-plan.md, unit U3).\n * Split out of the route module to keep it under the repo's 600-line file cap.\n */\n\n/**\n * Preview-route-local adapter surface for the auto-proxy feature. Both\n * fields are optional so any existing `StudioApiAdapter` value remains\n * structurally assignable without editing the shared interface:\n * `autoProxy` defaults to true (on) when omitted — a later unit wires the\n * CLI `--no-proxy` flag / `hyperframes.json` setting through it;\n * `mediaCodecProbeCache` lets a host share one probe cache across\n * preview/play/static-server surfaces instead of each constructing its own.\n */\nexport type PreviewApiAdapter = StudioApiAdapter & {\n autoProxy?: boolean;\n mediaCodecProbeCache?: MediaCodecProbeCache;\n};\n\nexport function isAutoProxyEnabled(adapter: PreviewApiAdapter): boolean {\n return adapter.autoProxy !== false;\n}\n\n/** One probe cache per server instance — construct once in `registerPreviewRoutes`\n * and reuse across every request so the mtime-cache benefit in\n * `scanProjectMediaCodecMap` actually applies. A host that wants to share the\n * cache across other surfaces (play, static project server) can pass its own\n * via `adapter.mediaCodecProbeCache`. */\nexport function resolvePreviewMediaCodecProbeCache(\n adapter: PreviewApiAdapter,\n): MediaCodecProbeCache {\n return adapter.mediaCodecProbeCache ?? createMediaCodecProbeCache();\n}\n\n/**\n * ETag salt for `?hf-proxy=` asset requests, mirroring `variablesEtagSalt` in\n * preview.ts: salted by the raw param value plus the transcoder's params\n * version, so a future proxy-recipe change (which bumps `PROXY_PARAMS_VERSION`)\n * or a different proxy variant invalidates cached 304s without needing to\n * touch the proxy file itself.\n */\nexport function proxyEtagSalt(raw: string | undefined): string {\n if (raw === undefined) return \"\";\n return `:proxy:${raw}:${PROXY_PARAMS_VERSION}`;\n}\n\n// Mirrors `injectScriptTagIntoHead` in routes/preview.ts (kept local rather\n// than imported to avoid a helpers → routes dependency edge for one\n// two-line utility).\nfunction injectScriptTagIntoHead(html: string, scriptTag: string): string {\n if (html.includes(\"</head>\")) return html.replace(\"</head>\", `${scriptTag}\\n</head>`);\n return `${scriptTag}\\n${html}`;\n}\n\n/**\n * Injects `window.__HF_MEDIA_CODEC_MAP__` (the U1 codec-facts scan) into\n * served composition HTML, and fire-and-forget pre-warms `resolveProxy` for\n * every browser-hostile entry so an element's proactive swap usually hits a\n * warm cache (KTD: protects the per-origin connection budget under held\n * responses). No second concurrency limiter here — the transcoder's own\n * global bound throttles both pre-warm and element-triggered calls.\n * Pre-warm failures are swallowed; an actual `?hf-proxy=` request surfaces\n * them as a 502. Alpha-bearing entries pre-warm their VP8/WebM variant.\n *\n * The single shared implementation for every auto-proxy surface — the studio\n * preview route (via `injectMediaCodecMap` below) and the CLI's composition /\n * static project servers (via the `./media-proxy-preview` subpath export).\n * Empty maps leave HTML untouched, preserving the normal no-hostile-media\n * preview path. On-demand proxy requests enforce the same eligibility gate.\n */\nexport async function injectMediaCodecMapIntoHtml(\n html: string,\n projectDir: string,\n htmlSources: HtmlSourceLike[],\n probeCache?: MediaCodecProbeCache,\n): Promise<string> {\n let map: MediaCodecMap;\n try {\n map = await scanProjectMediaCodecMap(\n projectDir,\n htmlSources,\n probeCache ? { cache: probeCache } : {},\n );\n } catch {\n // Best-effort: a scan failure must never block serving the page.\n return html;\n }\n if (Object.keys(map).length === 0) return html;\n for (const [rootRelativePathname, facts] of Object.entries(map)) {\n if (!facts.browserHostile) continue;\n resolveProxy(\n projectDir,\n resolve(projectDir, rootRelativePathname.replace(/^\\/+/, \"\")),\n proxyVariantFor(facts),\n ).catch(() => {\n // Swallowed: the pre-warm is best-effort. A real `?hf-proxy=` request\n // for this asset re-attempts the transcode and reports failure (502).\n });\n }\n // <-escape prevents a src path containing \"</script>\" from breaking out of\n // the injected tag, mirroring injectPreviewVariables in helpers/previewVariables.ts.\n const json = JSON.stringify(map)\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n const tag = `<script data-hf-media-codec-map>window.__HF_MEDIA_CODEC_MAP__=${json};</script>`;\n return injectScriptTagIntoHead(html, tag);\n}\n\n/**\n * Adapter-aware wrapper used by the studio preview routes: skipped entirely\n * (no scan, no injection) when auto-proxy is off for this adapter.\n */\nexport async function injectMediaCodecMap(\n html: string,\n adapter: PreviewApiAdapter,\n projectDir: string,\n compSrcPath: string,\n probeCache: MediaCodecProbeCache,\n): Promise<string> {\n if (!isAutoProxyEnabled(adapter)) return html;\n return injectMediaCodecMapIntoHtml(html, projectDir, [{ html, compSrcPath }], probeCache);\n}\n"],"mappings":";;;;;;;;;;;AAAA,SAAS,eAAe;AAgCjB,SAAS,mBAAmB,SAAqC;AACtE,SAAO,QAAQ,cAAc;AAC/B;AAOO,SAAS,mCACd,SACsB;AACtB,SAAO,QAAQ,wBAAwB,2BAA2B;AACpE;AASO,SAAS,cAAc,KAAiC;AAC7D,MAAI,QAAQ,OAAW,QAAO;AAC9B,SAAO,UAAU,GAAG,IAAI,oBAAoB;AAC9C;AAKA,SAAS,wBAAwB,MAAc,WAA2B;AACxE,MAAI,KAAK,SAAS,SAAS,EAAG,QAAO,KAAK,QAAQ,WAAW,GAAG,SAAS;AAAA,QAAW;AACpF,SAAO,GAAG,SAAS;AAAA,EAAK,IAAI;AAC9B;AAkBA,eAAsB,4BACpB,MACA,YACA,aACA,YACiB;AACjB,MAAI;AACJ,MAAI;AACF,UAAM,MAAM;AAAA,MACV;AAAA,MACA;AAAA,MACA,aAAa,EAAE,OAAO,WAAW,IAAI,CAAC;AAAA,IACxC;AAAA,EACF,QAAQ;AAEN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,KAAK,GAAG,EAAE,WAAW,EAAG,QAAO;AAC1C,aAAW,CAAC,sBAAsB,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC/D,QAAI,CAAC,MAAM,eAAgB;AAC3B;AAAA,MACE;AAAA,MACA,QAAQ,YAAY,qBAAqB,QAAQ,QAAQ,EAAE,CAAC;AAAA,MAC5D,gBAAgB,KAAK;AAAA,IACvB,EAAE,MAAM,MAAM;AAAA,IAGd,CAAC;AAAA,EACH;AAGA,QAAM,OAAO,KAAK,UAAU,GAAG,EAC5B,QAAQ,MAAM,SAAS,EACvB,QAAQ,WAAW,SAAS,EAC5B,QAAQ,WAAW,SAAS;AAC/B,QAAM,MAAM,iEAAiE,IAAI;AACjF,SAAO,wBAAwB,MAAM,GAAG;AAC1C;AAMA,eAAsB,oBACpB,MACA,SACA,YACA,aACA,YACiB;AACjB,MAAI,CAAC,mBAAmB,OAAO,EAAG,QAAO;AACzC,SAAO,4BAA4B,MAAM,YAAY,CAAC,EAAE,MAAM,YAAY,CAAC,GAAG,UAAU;AAC1F;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
splitElementInHtml,
|
|
11
11
|
unwrapElementsFromHtml,
|
|
12
12
|
wrapElementsInHtml
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-AG4UCNIY.js";
|
|
14
14
|
import {
|
|
15
15
|
getElementScreenshotClip
|
|
16
16
|
} from "./chunk-W2SBTCO2.js";
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
isAutoProxyEnabled,
|
|
20
20
|
proxyEtagSalt,
|
|
21
21
|
resolvePreviewMediaCodecProbeCache
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-C6CSSUAY.js";
|
|
23
23
|
import {
|
|
24
24
|
ProxyCapacityError,
|
|
25
25
|
ProxyTranscodeError,
|
|
@@ -364,12 +364,12 @@ import {
|
|
|
364
364
|
writeSync,
|
|
365
365
|
mkdirSync as mkdirSync3,
|
|
366
366
|
unlinkSync as unlinkSync2,
|
|
367
|
-
rmSync as
|
|
367
|
+
rmSync as rmSync3,
|
|
368
368
|
statSync as statSync2,
|
|
369
|
-
renameSync,
|
|
369
|
+
renameSync as renameSync2,
|
|
370
370
|
readdirSync as readdirSync4
|
|
371
371
|
} from "fs";
|
|
372
|
-
import { resolve as resolve3, dirname as
|
|
372
|
+
import { resolve as resolve3, dirname as dirname3, join as join6 } from "path";
|
|
373
373
|
|
|
374
374
|
// src/helpers/mime.ts
|
|
375
375
|
var MIME_TYPES = {
|
|
@@ -419,8 +419,17 @@ function isAudioFile(name) {
|
|
|
419
419
|
|
|
420
420
|
// src/helpers/waveform.ts
|
|
421
421
|
import { spawn } from "child_process";
|
|
422
|
-
import {
|
|
423
|
-
|
|
422
|
+
import {
|
|
423
|
+
existsSync as existsSync2,
|
|
424
|
+
writeFileSync,
|
|
425
|
+
mkdirSync,
|
|
426
|
+
lstatSync as lstatSync2,
|
|
427
|
+
mkdtempSync,
|
|
428
|
+
renameSync,
|
|
429
|
+
rmSync,
|
|
430
|
+
statSync
|
|
431
|
+
} from "fs";
|
|
432
|
+
import { dirname, join as join3 } from "path";
|
|
424
433
|
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
425
434
|
var SAMPLE_RATE = 4e3;
|
|
426
435
|
var PEAK_COUNT = 4e3;
|
|
@@ -492,15 +501,39 @@ async function generateWaveformCache(projectDir, assetPath) {
|
|
|
492
501
|
const stats = statSync(audioPath);
|
|
493
502
|
const cacheDir = join3(projectDir, ".waveform-cache");
|
|
494
503
|
const cachePath = join3(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
495
|
-
if (existsSync2(cachePath)) return;
|
|
504
|
+
if (isWaveformCacheDirectory(cacheDir) && existsSync2(cachePath)) return;
|
|
496
505
|
const peaks = await decodeAudioPeaks(audioPath);
|
|
497
|
-
|
|
498
|
-
|
|
506
|
+
writeWaveformCache(cachePath, peaks);
|
|
507
|
+
}
|
|
508
|
+
function isWaveformCacheDirectory(cacheDir) {
|
|
509
|
+
return lstatSync2(cacheDir, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
510
|
+
}
|
|
511
|
+
function writeWaveformCache(cachePath, peaks) {
|
|
512
|
+
const cacheDir = dirname(cachePath);
|
|
513
|
+
try {
|
|
514
|
+
mkdirSync(cacheDir, { mode: 448 });
|
|
515
|
+
} catch (error) {
|
|
516
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
517
|
+
}
|
|
518
|
+
if (!isWaveformCacheDirectory(cacheDir)) {
|
|
519
|
+
throw new Error("Waveform cache must be a directory, not a symlink");
|
|
520
|
+
}
|
|
521
|
+
const stagingDir = mkdtempSync(join3(cacheDir, ".waveform-"));
|
|
522
|
+
try {
|
|
523
|
+
const stagingPath = join3(stagingDir, "peaks.json");
|
|
524
|
+
writeFileSync(stagingPath, JSON.stringify(peaks), { flag: "wx" });
|
|
525
|
+
renameSync(stagingPath, cachePath);
|
|
526
|
+
} finally {
|
|
527
|
+
try {
|
|
528
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
531
|
+
}
|
|
499
532
|
}
|
|
500
533
|
|
|
501
534
|
// src/helpers/mediaValidation.ts
|
|
502
535
|
import { spawnSync } from "child_process";
|
|
503
|
-
import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
536
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
504
537
|
import { tmpdir } from "os";
|
|
505
538
|
import { basename, join as join4 } from "path";
|
|
506
539
|
var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
|
|
@@ -539,13 +572,13 @@ function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
|
539
572
|
}
|
|
540
573
|
}
|
|
541
574
|
function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
|
|
542
|
-
const tempDir =
|
|
575
|
+
const tempDir = mkdtempSync2(join4(tmpdir(), "hyperframes-upload-"));
|
|
543
576
|
const tempPath = join4(tempDir, basename(fileName));
|
|
544
577
|
try {
|
|
545
578
|
writeFileSync2(tempPath, buffer);
|
|
546
579
|
return validateUploadedMedia(tempPath, runner);
|
|
547
580
|
} finally {
|
|
548
|
-
|
|
581
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
549
582
|
}
|
|
550
583
|
}
|
|
551
584
|
|
|
@@ -687,7 +720,7 @@ import { parseHTML as parseHTML2 } from "linkedom";
|
|
|
687
720
|
// src/helpers/compositionInsertion.ts
|
|
688
721
|
import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync } from "fs";
|
|
689
722
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
690
|
-
import { dirname, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
|
|
723
|
+
import { dirname as dirname2, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
|
|
691
724
|
import { parseHTML } from "linkedom";
|
|
692
725
|
var CompositionInsertionError = class extends Error {
|
|
693
726
|
constructor(message, status) {
|
|
@@ -742,7 +775,7 @@ function canonicalDependency(projectDir, ownerAbs, sourcePath) {
|
|
|
742
775
|
validateSourcePath(sourcePath);
|
|
743
776
|
return canonicalProjectPath(
|
|
744
777
|
projectDir,
|
|
745
|
-
resolveWithinProject(projectDir, relative3(projectDir, resolve2(
|
|
778
|
+
resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname2(ownerAbs), sourcePath)))
|
|
746
779
|
);
|
|
747
780
|
}
|
|
748
781
|
function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
|
|
@@ -815,7 +848,7 @@ function uniqueHostId(root, base) {
|
|
|
815
848
|
return `${base}_${suffix}`;
|
|
816
849
|
}
|
|
817
850
|
function relativeSourcePath(targetAbs, sourceAbs) {
|
|
818
|
-
return relative3(
|
|
851
|
+
return relative3(dirname2(targetAbs), sourceAbs).split(sep2).join("/");
|
|
819
852
|
}
|
|
820
853
|
function insertCompositionIntoSource(input) {
|
|
821
854
|
const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
|
|
@@ -1153,7 +1186,7 @@ async function parseMutationBody(c) {
|
|
|
1153
1186
|
return { target: body.target, body };
|
|
1154
1187
|
}
|
|
1155
1188
|
function ensureDir(filePath) {
|
|
1156
|
-
const dir =
|
|
1189
|
+
const dir = dirname3(filePath);
|
|
1157
1190
|
if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
|
|
1158
1191
|
}
|
|
1159
1192
|
function generateCopyPath(projectDir, originalPath) {
|
|
@@ -2457,7 +2490,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2457
2490
|
const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
2458
2491
|
if (backup.error) console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
|
|
2459
2492
|
if (stat.isDirectory()) {
|
|
2460
|
-
|
|
2493
|
+
rmSync3(res.absPath, { recursive: true });
|
|
2461
2494
|
} else {
|
|
2462
2495
|
unlinkSync2(res.absPath);
|
|
2463
2496
|
}
|
|
@@ -2953,7 +2986,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2953
2986
|
return c.json({ error: "already exists" }, 409);
|
|
2954
2987
|
}
|
|
2955
2988
|
ensureDir(newAbs);
|
|
2956
|
-
|
|
2989
|
+
renameSync2(res.absPath, newAbs);
|
|
2957
2990
|
const updatedFiles = updateReferences(res.project.dir, res.filePath, body.newPath);
|
|
2958
2991
|
return c.json({ ok: true, path: body.newPath, updatedReferences: updatedFiles });
|
|
2959
2992
|
});
|
|
@@ -3073,7 +3106,8 @@ function registerFileRoutes(api, adapter) {
|
|
|
3073
3106
|
}
|
|
3074
3107
|
|
|
3075
3108
|
// src/routes/preview.ts
|
|
3076
|
-
import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
|
|
3109
|
+
import { createReadStream, existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
|
|
3110
|
+
import { Readable } from "stream";
|
|
3077
3111
|
import { join as join8, resolve as resolve4 } from "path";
|
|
3078
3112
|
import { createHash as createHash3 } from "crypto";
|
|
3079
3113
|
import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
|
|
@@ -3330,6 +3364,22 @@ function isVariablesPayload(value) {
|
|
|
3330
3364
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3331
3365
|
}
|
|
3332
3366
|
|
|
3367
|
+
// src/helpers/previewVariables.ts
|
|
3368
|
+
function injectPreviewVariables(html, values) {
|
|
3369
|
+
const json = JSON.stringify(values).replace(/</g, "\\u003c");
|
|
3370
|
+
const tag = `<script data-hf-preview-variables>window.__hfVariables=${json};</script>`;
|
|
3371
|
+
for (const pattern of [/<head/i, /<html/i, /^\s*<!doctype/i]) {
|
|
3372
|
+
const match = pattern.exec(html);
|
|
3373
|
+
if (match) {
|
|
3374
|
+
const end = html.indexOf(">", match.index + match[0].length);
|
|
3375
|
+
if (end < 0) continue;
|
|
3376
|
+
const at = end + 1;
|
|
3377
|
+
return html.slice(0, at) + tag + html.slice(at);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
return tag + html;
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3333
3383
|
// src/routes/preview.ts
|
|
3334
3384
|
var PROJECT_SIGNATURE_META = "hyperframes-project-signature";
|
|
3335
3385
|
var GSAP_CDN_VERSION = "3.15.0";
|
|
@@ -3449,18 +3499,6 @@ function injectGsapCdnFallback(html) {
|
|
|
3449
3499
|
if (html.includes("<head>")) return html.replace("<head>", "<head>" + GSAP_CDN_FALLBACK_SCRIPT);
|
|
3450
3500
|
return GSAP_CDN_FALLBACK_SCRIPT + html;
|
|
3451
3501
|
}
|
|
3452
|
-
function injectPreviewVariables(html, values) {
|
|
3453
|
-
const json = JSON.stringify(values).replace(/</g, "\\u003c");
|
|
3454
|
-
const tag = `<script data-hf-preview-variables>window.__hfVariables=${json};</script>`;
|
|
3455
|
-
for (const pattern of [/<head[^>]*>/i, /<html[^>]*>/i, /^\s*<!doctype[^>]*>/i]) {
|
|
3456
|
-
const match = pattern.exec(html);
|
|
3457
|
-
if (match) {
|
|
3458
|
-
const at = match.index + match[0].length;
|
|
3459
|
-
return html.slice(0, at) + tag + html.slice(at);
|
|
3460
|
-
}
|
|
3461
|
-
}
|
|
3462
|
-
return tag + html;
|
|
3463
|
-
}
|
|
3464
3502
|
function parsePreviewVariablesParam(raw) {
|
|
3465
3503
|
if (raw === void 0 || raw === "") return { ok: true, values: null };
|
|
3466
3504
|
let parsed;
|
|
@@ -3710,29 +3748,39 @@ ${runtimeTag}`;
|
|
|
3710
3748
|
}
|
|
3711
3749
|
servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
|
|
3712
3750
|
}
|
|
3713
|
-
const
|
|
3714
|
-
const totalSize =
|
|
3751
|
+
const textBuffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : null;
|
|
3752
|
+
const totalSize = textBuffer ? textBuffer.length : statSync3(servedPath).size;
|
|
3753
|
+
const bodyFor = (start, end) => textBuffer ? new Uint8Array(textBuffer.subarray(start, end + 1)) : (
|
|
3754
|
+
// Node's web stream type and the DOM one do not overlap for tsc on
|
|
3755
|
+
// every platform's lib set; the double cast is the documented bridge.
|
|
3756
|
+
Readable.toWeb(
|
|
3757
|
+
createReadStream(servedPath, { start, end })
|
|
3758
|
+
)
|
|
3759
|
+
);
|
|
3715
3760
|
const rangeHeader = c.req.header("Range");
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
headers: {
|
|
3726
|
-
...cacheHeaders,
|
|
3727
|
-
"Content-Type": servedContentType,
|
|
3728
|
-
"Content-Range": `bytes ${start}-${safeEnd}/${totalSize}`,
|
|
3729
|
-
"Accept-Ranges": "bytes",
|
|
3730
|
-
"Content-Length": String(chunkSize)
|
|
3731
|
-
}
|
|
3761
|
+
const match = rangeHeader ? /bytes=(\d+)-(\d*)/.exec(rangeHeader) : null;
|
|
3762
|
+
if (match) {
|
|
3763
|
+
const start = parseInt(match[1], 10);
|
|
3764
|
+
const end = match[2] ? parseInt(match[2], 10) : totalSize - 1;
|
|
3765
|
+
const safeEnd = Math.min(end, totalSize - 1);
|
|
3766
|
+
if (start > safeEnd) {
|
|
3767
|
+
return new Response(null, {
|
|
3768
|
+
status: 416,
|
|
3769
|
+
headers: { ...cacheHeaders, "Content-Range": `bytes */${totalSize}` }
|
|
3732
3770
|
});
|
|
3733
3771
|
}
|
|
3772
|
+
return new Response(bodyFor(start, safeEnd), {
|
|
3773
|
+
status: 206,
|
|
3774
|
+
headers: {
|
|
3775
|
+
...cacheHeaders,
|
|
3776
|
+
"Content-Type": servedContentType,
|
|
3777
|
+
"Content-Range": `bytes ${start}-${safeEnd}/${totalSize}`,
|
|
3778
|
+
"Accept-Ranges": "bytes",
|
|
3779
|
+
"Content-Length": String(safeEnd - start + 1)
|
|
3780
|
+
}
|
|
3781
|
+
});
|
|
3734
3782
|
}
|
|
3735
|
-
return new Response(
|
|
3783
|
+
return new Response(totalSize > 0 ? bodyFor(0, totalSize - 1) : null, {
|
|
3736
3784
|
headers: {
|
|
3737
3785
|
...cacheHeaders,
|
|
3738
3786
|
"Content-Type": servedContentType,
|
|
@@ -4037,8 +4085,8 @@ import {
|
|
|
4037
4085
|
mkdirSync as mkdirSync5,
|
|
4038
4086
|
readFileSync as readFileSync11,
|
|
4039
4087
|
readdirSync as readdirSync6,
|
|
4040
|
-
renameSync as
|
|
4041
|
-
rmSync as
|
|
4088
|
+
renameSync as renameSync3,
|
|
4089
|
+
rmSync as rmSync4,
|
|
4042
4090
|
statSync as statSync5,
|
|
4043
4091
|
unlinkSync as unlinkSync4,
|
|
4044
4092
|
writeFileSync as writeFileSync6
|
|
@@ -4172,7 +4220,7 @@ function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
|
|
|
4172
4220
|
const retained = [];
|
|
4173
4221
|
for (const file of files) {
|
|
4174
4222
|
if (!protectedPaths.has(file.path) && now - file.mtimeMs > THUMBNAIL_CACHE_MAX_AGE_MS) {
|
|
4175
|
-
|
|
4223
|
+
rmSync4(file.path, { force: true });
|
|
4176
4224
|
} else {
|
|
4177
4225
|
retained.push(file);
|
|
4178
4226
|
}
|
|
@@ -4192,9 +4240,9 @@ function writeThumbnailAtomically(path, buffer) {
|
|
|
4192
4240
|
const temporaryPath = `${path}.${process.pid}.${randomUUID3()}.tmp`;
|
|
4193
4241
|
try {
|
|
4194
4242
|
writeFileSync6(temporaryPath, buffer, { flag: "wx" });
|
|
4195
|
-
|
|
4243
|
+
renameSync3(temporaryPath, path);
|
|
4196
4244
|
} finally {
|
|
4197
|
-
|
|
4245
|
+
rmSync4(temporaryPath, { force: true });
|
|
4198
4246
|
}
|
|
4199
4247
|
}
|
|
4200
4248
|
function registerThumbnailRoutes(api, adapter) {
|
|
@@ -4330,7 +4378,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4330
4378
|
}
|
|
4331
4379
|
|
|
4332
4380
|
// src/routes/waveform.ts
|
|
4333
|
-
import { existsSync as existsSync9, readFileSync as readFileSync12,
|
|
4381
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12, statSync as statSync6 } from "fs";
|
|
4334
4382
|
import { join as join12 } from "path";
|
|
4335
4383
|
function registerWaveformRoutes(api, adapter) {
|
|
4336
4384
|
api.get("/projects/:id/waveform/*", async (c) => {
|
|
@@ -4344,12 +4392,12 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4344
4392
|
if (!stats) return c.json({ error: "file not found" }, 404);
|
|
4345
4393
|
const cacheDir = join12(project.dir, ".waveform-cache");
|
|
4346
4394
|
const cachePath = join12(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
4347
|
-
|
|
4348
|
-
|
|
4395
|
+
try {
|
|
4396
|
+
if (isWaveformCacheDirectory(cacheDir) && existsSync9(cachePath)) {
|
|
4349
4397
|
const peaks2 = JSON.parse(readFileSync12(cachePath, "utf-8"));
|
|
4350
4398
|
return c.json({ peaks: peaks2 });
|
|
4351
|
-
} catch {
|
|
4352
4399
|
}
|
|
4400
|
+
} catch {
|
|
4353
4401
|
}
|
|
4354
4402
|
let peaks;
|
|
4355
4403
|
try {
|
|
@@ -4358,8 +4406,7 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4358
4406
|
return c.json({ error: "failed to decode audio" }, 500);
|
|
4359
4407
|
}
|
|
4360
4408
|
try {
|
|
4361
|
-
|
|
4362
|
-
writeFileSync7(cachePath, JSON.stringify(peaks));
|
|
4409
|
+
writeWaveformCache(cachePath, peaks);
|
|
4363
4410
|
} catch {
|
|
4364
4411
|
}
|
|
4365
4412
|
return c.json({ peaks });
|
|
@@ -4630,8 +4677,8 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
4630
4677
|
|
|
4631
4678
|
// src/routes/media.ts
|
|
4632
4679
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
4633
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
4634
|
-
import { basename as basename2, dirname as
|
|
4680
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
|
|
4681
|
+
import { basename as basename2, dirname as dirname4, extname as extname2, join as join13 } from "path";
|
|
4635
4682
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4636
4683
|
".mp4",
|
|
4637
4684
|
".mov",
|
|
@@ -4775,8 +4822,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
4775
4822
|
return c.json({ error: "forbidden" }, 403);
|
|
4776
4823
|
}
|
|
4777
4824
|
}
|
|
4778
|
-
|
|
4779
|
-
if (backgroundOutputPath)
|
|
4825
|
+
mkdirSync6(dirname4(outputPath), { recursive: true });
|
|
4826
|
+
if (backgroundOutputPath) mkdirSync6(dirname4(backgroundOutputPath), { recursive: true });
|
|
4780
4827
|
const jobId = makeJobId(project.id, mediaJobs);
|
|
4781
4828
|
const state = adapter.startBackgroundRemoval({
|
|
4782
4829
|
project,
|