mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(producer): rewrite relative asset paths when inlining sub-compositions (#166)
## Summary - **Fixes**: `<img src="../icon.svg">` and similar `../` relative asset references in sub-compositions resolve to 404 after inlining into the root document - **Unifies**: Both `hyperframes preview` (core bundler) and `hyperframes render` (producer) now use the same shared logic — no duplication ## Root cause When inlining a sub-composition at `compositions/scene.html` into root `index.html`, a relative path like `../icon.svg` is correct from `compositions/` (it points to project root) but after inlining, `../` escapes the project directory. ## Fix Extracts path rewriting into a shared `rewriteSubCompPaths.ts` utility in `@hyperframes/core`, used by both the bundler and the producer. **Only rewrites paths starting with** **`../`** — plain relative paths like `assets/foo.svg` are already correct from the root perspective and must not be rewritten (this was the regression cause in `overlay-montage-prod`: sub-composition asset refs like `assets/notch.svg` were incorrectly being rewritten to `compositions/assets/notch.svg`). ## Regression fix The earlier version of this fix (now in history) rewrote ALL relative paths including `assets/foo.svg`, breaking the `overlay-montage-prod` regression test. This PR fixes that by scoping rewrites to `../`\-prefixed paths only. ## Test plan - [x] `../icon.svg` in sub-composition renders correctly in both preview and render - [x] `assets/foo.svg` (no `../`) in sub-composition still resolves correctly — not rewritten - [x] `overlay-montage-prod` regression test passes - [x] All other regression shards pass
This commit is contained in:
@@ -3,6 +3,7 @@ import { join, resolve, isAbsolute, sep } from "path";
|
||||
import * as cheerio from "cheerio";
|
||||
import { transformSync } from "esbuild";
|
||||
import { compileHtml, type MediaDurationProber } from "./htmlCompiler";
|
||||
import { rewriteAssetPaths } from "./rewriteSubCompPaths";
|
||||
import { validateHyperframeHtmlContract } from "./staticGuard";
|
||||
|
||||
/** Resolve a relative path within projectDir, rejecting traversal outside it. */
|
||||
@@ -432,6 +433,20 @@ export async function bundleToSingleHtml(
|
||||
$content(s).remove();
|
||||
});
|
||||
|
||||
// Rewrite relative asset paths before inlining so ../foo.svg from
|
||||
// compositions/ resolves correctly when the content moves to root.
|
||||
const $assetEls = $innerRoot.length
|
||||
? $innerRoot.find("[src], [href]")
|
||||
: $content("[src], [href]");
|
||||
rewriteAssetPaths(
|
||||
$assetEls.toArray(),
|
||||
src,
|
||||
(el, attr) => $content(el).attr(attr),
|
||||
(el, attr, val) => {
|
||||
$content(el).attr(attr, val);
|
||||
},
|
||||
);
|
||||
|
||||
if ($innerRoot.length) {
|
||||
const innerCompId = $innerRoot.attr("data-composition-id");
|
||||
const innerW = $innerRoot.attr("data-width");
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Rewrite relative asset paths in sub-composition content so they resolve
|
||||
* correctly after the content is inlined into the root document.
|
||||
*
|
||||
* A sub-composition at "compositions/scene.html" referencing "../icon.svg"
|
||||
* means the project root — but after inlining into root index.html, the
|
||||
* "../" escapes the project directory and causes 404s. This function
|
||||
* resolves each relative path against the sub-composition's directory,
|
||||
* then normalizes it to be relative to the project root.
|
||||
*
|
||||
* Used by both the core bundler (preview) and the producer compiler (render)
|
||||
* to ensure consistent behavior.
|
||||
*/
|
||||
|
||||
import { join, resolve, dirname } from "path";
|
||||
|
||||
/** Attributes that may contain relative asset paths. */
|
||||
const PATH_ATTRS = ["src", "href"] as const;
|
||||
|
||||
/** Protocols and prefixes that should never be rewritten. */
|
||||
function isAbsoluteOrSpecial(val: string): boolean {
|
||||
return (
|
||||
!val ||
|
||||
val.startsWith("http://") ||
|
||||
val.startsWith("https://") ||
|
||||
val.startsWith("//") ||
|
||||
val.startsWith("data:") ||
|
||||
val.startsWith("#")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true only for paths that traverse up with `../`.
|
||||
* Plain relative paths like `assets/foo.svg` are already correct from the
|
||||
* root perspective — the browser resolves them against the served root, which
|
||||
* is the project root, so they don't need rewriting.
|
||||
*/
|
||||
function needsRewrite(val: string): boolean {
|
||||
return val.startsWith("../") || val === "..";
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite a single relative path from a sub-composition's context to the
|
||||
* project root context.
|
||||
*
|
||||
* @param compSrcPath - The `data-composition-src` value (e.g. "compositions/scene.html")
|
||||
* @param relativePath - The asset path to rewrite (e.g. "../icon.svg")
|
||||
* @returns The rewritten path relative to project root (e.g. "icon.svg"), or
|
||||
* the original path if no rewriting is needed.
|
||||
*/
|
||||
export function rewriteAssetPath(compSrcPath: string, relativePath: string): string {
|
||||
if (isAbsoluteOrSpecial(relativePath)) return relativePath;
|
||||
if (!needsRewrite(relativePath)) return relativePath;
|
||||
const compDir = dirname(compSrcPath);
|
||||
if (!compDir || compDir === ".") return relativePath;
|
||||
const resolved = join(compDir, relativePath);
|
||||
const normalized = resolve("/", resolved).slice(1);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite all relative `src` and `href` attributes on elements within a
|
||||
* DOM tree, adjusting paths from the sub-composition's directory context
|
||||
* to the project root.
|
||||
*
|
||||
* @param elements - Iterable of DOM elements to scan (e.g. from querySelectorAll)
|
||||
* @param compSrcPath - The `data-composition-src` value
|
||||
* @param getAttr - Function to read an attribute from an element
|
||||
* @param setAttr - Function to set an attribute on an element
|
||||
*/
|
||||
export function rewriteAssetPaths<T>(
|
||||
elements: Iterable<T>,
|
||||
compSrcPath: string,
|
||||
getAttr: (el: T, attr: string) => string | null | undefined,
|
||||
setAttr: (el: T, attr: string, value: string) => void,
|
||||
): void {
|
||||
const compDir = dirname(compSrcPath);
|
||||
if (!compDir || compDir === ".") return;
|
||||
|
||||
for (const el of elements) {
|
||||
for (const attr of PATH_ATTRS) {
|
||||
const val = (getAttr(el, attr) || "").trim();
|
||||
if (isAbsoluteOrSpecial(val)) continue;
|
||||
if (!needsRewrite(val)) continue;
|
||||
const rewritten = join(compDir, val);
|
||||
const normalized = resolve("/", rewritten).slice(1);
|
||||
if (normalized !== val) {
|
||||
setAttr(el, attr, normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,7 @@ export type {
|
||||
HyperframeLinterOptions,
|
||||
} from "./lint/types";
|
||||
export { lintHyperframeHtml } from "./lint/hyperframeLinter";
|
||||
export { rewriteAssetPaths, rewriteAssetPath } from "./compiler/rewriteSubCompPaths";
|
||||
|
||||
// Inline scripts
|
||||
export {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
clampDurations,
|
||||
type ResolvedDuration,
|
||||
type UnresolvedElement,
|
||||
rewriteAssetPaths,
|
||||
} from "@hyperframes/core";
|
||||
import { extractVideoMetadata, extractAudioMetadata } from "../utils/ffprobe.js";
|
||||
import {
|
||||
@@ -578,6 +579,16 @@ function inlineSubCompositions(
|
||||
scriptEl.remove();
|
||||
}
|
||||
|
||||
// Rewrite relative asset paths before inlining so ../foo.svg from
|
||||
// compositions/ resolves correctly when the content moves to root.
|
||||
const rewriteTarget = innerRoot || contentDoc;
|
||||
rewriteAssetPaths(
|
||||
rewriteTarget.querySelectorAll("[src], [href]"),
|
||||
srcPath,
|
||||
(el, attr) => (el.getAttribute(attr) || "").trim(),
|
||||
(el, attr, val) => el.setAttribute(attr, val),
|
||||
);
|
||||
|
||||
if (innerRoot) {
|
||||
const innerW = innerRoot.getAttribute("data-width");
|
||||
const innerH = innerRoot.getAttribute("data-height");
|
||||
|
||||
Reference in New Issue
Block a user