mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(cli): include encoded external assets in archives (#6266)
This commit is contained in:
@@ -130,7 +130,9 @@ immediately, update an existing link, or publish into a shared space.
|
||||
|
||||
A local video poster referenced with a relative path, such as
|
||||
`../shared/poster.png`, is copied into the published project even when it lives
|
||||
outside the project directory.
|
||||
outside the project directory. Percent-encoded paths work too, such as
|
||||
`../shared/poster%20art.svg` for `poster art.svg` or
|
||||
`../shared/%E5%B0%81%E9%9D%A2.svg` for `封面.svg`.
|
||||
|
||||
Before sharing source, remove secrets and confirm that every included media file
|
||||
can be shared. Tell a reviewer whether you need feedback on story, visuals,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, posix, relative } from "node:path";
|
||||
import AdmZip from "adm-zip";
|
||||
import { parseHTML } from "linkedom";
|
||||
|
||||
const authMocks = vi.hoisted(() => ({
|
||||
tryResolveCredential: vi.fn(),
|
||||
@@ -224,6 +225,51 @@ async function runAuthenticatedPublish(
|
||||
}
|
||||
|
||||
describe("createPublishArchive", () => {
|
||||
it.each([
|
||||
{ filename: "poster art.svg", reference: "poster%20art.svg" },
|
||||
{ filename: "封面.svg", reference: "%E5%B0%81%E9%9D%A2.svg" },
|
||||
{ filename: "100%.svg", reference: "100%.svg" },
|
||||
{ filename: "poster%20art.svg", reference: "poster%20art.svg" },
|
||||
{ filename: "poster#art.svg", reference: "poster%23art.svg" },
|
||||
{ filename: "poster#art.svg", reference: "poster#art.svg" },
|
||||
])("archives $filename referenced as $reference", ({ filename, reference }) => {
|
||||
const projectDir = makeProjectDir();
|
||||
const extDir = mkdtempSync(join(tmpdir(), "hf-ext-"));
|
||||
try {
|
||||
const entryPath = "compositions/scene.html";
|
||||
const htmlDir = join(projectDir, dirname(entryPath));
|
||||
mkdirSync(htmlDir, { recursive: true });
|
||||
stageExternalAsset(extDir, htmlDir, filename, "POSTER_DATA");
|
||||
if (reference !== filename) {
|
||||
stageExternalAsset(extDir, htmlDir, reference, "LITERAL_DECOY");
|
||||
}
|
||||
const assetPath = relative(htmlDir, join(extDir, reference)).replaceAll("\\", "/");
|
||||
const html = `<html><body><img src="${assetPath}"><video src="../clip.mp4" poster="${assetPath}"></video></body></html>`;
|
||||
writeFileSync(join(projectDir, "index.html"), "<html></html>");
|
||||
writeFileSync(join(projectDir, entryPath), html);
|
||||
writeFileSync(join(projectDir, "clip.mp4"), "MP4_DATA");
|
||||
|
||||
const archive = createPublishArchive(projectDir);
|
||||
const zip = new AdmZip(archive.buffer);
|
||||
const { document } = parseHTML(zip.readAsText(entryPath));
|
||||
const src = document.querySelector("img")?.getAttribute("src");
|
||||
if (!src) throw new Error("Archived image has no src");
|
||||
expect(document.querySelector("video")?.getAttribute("poster")).toBe(src);
|
||||
const url = new URL(src, `https://archive.example/${entryPath}`);
|
||||
expect(url.hash).toBe("");
|
||||
expect(url.search).toBe("");
|
||||
const archivedPath = decodeURIComponent(url.pathname.slice(1));
|
||||
expect(archivedPath).toMatch(/^_ext\//);
|
||||
expect(posix.basename(archivedPath)).toBe(filename);
|
||||
expect(zip.readFile(archivedPath)).toEqual(Buffer.from("POSTER_DATA"));
|
||||
expect(archive.fileCount).toBe(4);
|
||||
expect(readFileSync(join(projectDir, entryPath), "utf-8")).toBe(html);
|
||||
} finally {
|
||||
rmSync(projectDir, { recursive: true, force: true });
|
||||
rmSync(extDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["index.html", "compositions/scene.html"])(
|
||||
"includes external video posters referenced from %s",
|
||||
(entryPath) => {
|
||||
@@ -268,18 +314,21 @@ describe("createPublishArchive", () => {
|
||||
try {
|
||||
const posters = [
|
||||
"poster.png",
|
||||
"poster%20art.png",
|
||||
"https://cdn.example.com/poster.png",
|
||||
"https://cdn.example.com/poster%20art.png",
|
||||
"data:image/png;base64,AA==",
|
||||
];
|
||||
const html = `<html><body>${posters.map((poster) => `<video src="clip.mp4" poster="${poster}"></video>`).join("")}</body></html>`;
|
||||
writeFileSync(join(projectDir, "index.html"), html);
|
||||
writeFileSync(join(projectDir, "poster.png"), "PNG_DATA");
|
||||
writeFileSync(join(projectDir, "poster art.png"), "ENCODED_PNG_DATA");
|
||||
writeFileSync(join(projectDir, "clip.mp4"), "MP4_DATA");
|
||||
|
||||
const archive = createPublishArchive(projectDir);
|
||||
const zip = new AdmZip(archive.buffer);
|
||||
|
||||
expect(archive.fileCount).toBe(3);
|
||||
expect(archive.fileCount).toBe(4);
|
||||
expect(zip.readAsText("index.html")).toBe(html);
|
||||
expect(zip.getEntries().some((entry) => entry.entryName.startsWith("_ext/"))).toBe(false);
|
||||
} finally {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { parseHTML } from "linkedom";
|
||||
import AdmZip from "adm-zip";
|
||||
import ignore, { type Ignore } from "ignore";
|
||||
import { CSS_URL_RE, isNonRelativeUrl, isPathInside } from "@hyperframes/core";
|
||||
import { decodeUrlPathVariants, encodeUrlPath } from "@hyperframes/parsers";
|
||||
import { buildAuthHeaders } from "../auth/client.js";
|
||||
import { tryResolveCredential } from "../auth/index.js";
|
||||
import { isAuthError } from "../auth/errors.js";
|
||||
@@ -434,14 +435,17 @@ function tryResolveExternal(
|
||||
referrerAbsDir: string,
|
||||
): string | null {
|
||||
if (isNonRelativeUrl(rawPath)) return null;
|
||||
const absPath = resolve(referrerAbsDir, rawPath);
|
||||
if (isPathInside(absPath, ctx.absProjectDir)) return null;
|
||||
try {
|
||||
if (!existsSync(absPath) || !statSync(absPath).isFile()) return null;
|
||||
} catch {
|
||||
return null;
|
||||
for (const path of decodeUrlPathVariants(rawPath)) {
|
||||
const absPath = resolve(referrerAbsDir, path);
|
||||
if (isPathInside(absPath, ctx.absProjectDir)) return null;
|
||||
try {
|
||||
if (!existsSync(absPath) || !statSync(absPath).isFile()) continue;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
return encodeUrlPath(addExternalAsset(ctx, absPath));
|
||||
}
|
||||
return addExternalAsset(ctx, absPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Resolves a raw attribute or CSS `url()` value to the archive path or absolute
|
||||
|
||||
Reference in New Issue
Block a user