mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
feat(cli): make cloud archives size-aware
This commit is contained in:
@@ -208,6 +208,84 @@ describe("createPublishArchive", () => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("skips root render outputs but keeps same-named nested source directories", () => {
|
||||
const dir = makeProjectDir();
|
||||
try {
|
||||
writeFileSync(join(dir, "index.html"), "<html></html>", "utf-8");
|
||||
mkdirSync(join(dir, "renders"));
|
||||
writeFileSync(join(dir, "renders", "old.mp4"), "render-output", "utf-8");
|
||||
mkdirSync(join(dir, "snapshots"));
|
||||
writeFileSync(join(dir, "snapshots", "frame.png"), "snapshot-output", "utf-8");
|
||||
mkdirSync(join(dir, "assets", "renders"), { recursive: true });
|
||||
writeFileSync(join(dir, "assets", "renders", "source.mp4"), "source", "utf-8");
|
||||
|
||||
const zip = new AdmZip(createPublishArchive(dir).buffer);
|
||||
const entries = zip.getEntries().map((entry) => entry.entryName);
|
||||
|
||||
expect(entries).toContain("assets/renders/source.mp4");
|
||||
expect(entries).not.toContain("renders/old.mp4");
|
||||
expect(entries).not.toContain("snapshots/frame.png");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("applies gitignore-style rules from .hyperframesignore", () => {
|
||||
const dir = makeProjectDir();
|
||||
try {
|
||||
writeFileSync(join(dir, "index.html"), "<html></html>", "utf-8");
|
||||
mkdirSync(join(dir, "exports"));
|
||||
writeFileSync(join(dir, "exports", "draft.mp4"), "draft", "utf-8");
|
||||
mkdirSync(join(dir, "assets"));
|
||||
writeFileSync(join(dir, "assets", "discard.psd"), "discard", "utf-8");
|
||||
writeFileSync(join(dir, "assets", "keep.psd"), "keep", "utf-8");
|
||||
writeFileSync(
|
||||
join(dir, ".hyperframesignore"),
|
||||
["# Generated source files", "/exports/", "*.psd", "!assets/keep.psd", ""].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const zip = new AdmZip(createPublishArchive(dir).buffer);
|
||||
const entries = zip.getEntries().map((entry) => entry.entryName);
|
||||
|
||||
expect(entries).toContain("assets/keep.psd");
|
||||
expect(entries).not.toContain("exports/draft.mp4");
|
||||
expect(entries).not.toContain("assets/discard.psd");
|
||||
expect(entries).not.toContain(".hyperframesignore");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("allows .hyperframesignore to re-include a default output directory", () => {
|
||||
const dir = makeProjectDir();
|
||||
try {
|
||||
writeFileSync(join(dir, "index.html"), "<html></html>", "utf-8");
|
||||
mkdirSync(join(dir, "snapshots"));
|
||||
writeFileSync(join(dir, "snapshots", "reference.png"), "reference", "utf-8");
|
||||
writeFileSync(join(dir, ".hyperframesignore"), "!/snapshots/\n", "utf-8");
|
||||
|
||||
const zip = new AdmZip(createPublishArchive(dir).buffer);
|
||||
expect(zip.getEntries().map((entry) => entry.entryName)).toContain("snapshots/reference.png");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("fails clearly when .hyperframesignore excludes index.html", () => {
|
||||
const dir = makeProjectDir();
|
||||
try {
|
||||
writeFileSync(join(dir, "index.html"), "<html></html>", "utf-8");
|
||||
writeFileSync(join(dir, ".hyperframesignore"), "/index.html\n", "utf-8");
|
||||
|
||||
expect(() => createPublishArchive(dir)).toThrow(
|
||||
"Project archive must include index.html at the root. Check that .hyperframesignore does not exclude it.",
|
||||
);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("createPublishArchive (U6 cloud-render regression guard)", () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { basename, dirname, join, posix, relative, resolve } from "node:path";
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
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 { buildAuthHeaders } from "../auth/client.js";
|
||||
import { tryResolveCredential } from "../auth/index.js";
|
||||
@@ -9,6 +10,8 @@ import { writeProjectLink } from "./projectLink.js";
|
||||
|
||||
const IGNORED_DIRS = new Set([".git", "node_modules", "dist", ".next", "coverage"]);
|
||||
const IGNORED_FILES = new Set([".DS_Store", "Thumbs.db"]);
|
||||
const HYPERFRAMES_IGNORE_FILE = ".hyperframesignore";
|
||||
const DEFAULT_PROJECT_IGNORE = ["/renders/", "/snapshots/"];
|
||||
const PUBLISH_CONTENT_TYPE = "application/zip";
|
||||
const PUBLISH_METADATA_TIMEOUT_MS = 30_000;
|
||||
const PUBLISH_UPLOAD_MIN_TIMEOUT_MS = 120_000;
|
||||
@@ -174,7 +177,21 @@ function shouldIgnoreSegment(segment: string): boolean {
|
||||
return segment.startsWith(".") || IGNORED_DIRS.has(segment) || IGNORED_FILES.has(segment);
|
||||
}
|
||||
|
||||
function collectProjectFiles(rootDir: string, currentDir: string, paths: string[]): void {
|
||||
function createProjectIgnore(rootDir: string): Ignore {
|
||||
const matcher = ignore().add(DEFAULT_PROJECT_IGNORE);
|
||||
const ignorePath = join(rootDir, HYPERFRAMES_IGNORE_FILE);
|
||||
if (existsSync(ignorePath)) {
|
||||
matcher.add(readFileSync(ignorePath, "utf-8"));
|
||||
}
|
||||
return matcher;
|
||||
}
|
||||
|
||||
function collectProjectFiles(
|
||||
rootDir: string,
|
||||
currentDir: string,
|
||||
paths: string[],
|
||||
matcher: Ignore,
|
||||
): void {
|
||||
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
||||
if (shouldIgnoreSegment(entry.name)) continue;
|
||||
const absolutePath = join(currentDir, entry.name);
|
||||
@@ -182,11 +199,13 @@ function collectProjectFiles(rootDir: string, currentDir: string, paths: string[
|
||||
if (!relativePath) continue;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
collectProjectFiles(rootDir, absolutePath, paths);
|
||||
if (matcher.ignores(`${relativePath}/`)) continue;
|
||||
collectProjectFiles(rootDir, absolutePath, paths, matcher);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statSync(absolutePath).isFile()) continue;
|
||||
if (matcher.ignores(relativePath)) continue;
|
||||
paths.push(relativePath);
|
||||
}
|
||||
}
|
||||
@@ -388,9 +407,11 @@ export function localizeExternalAssets(
|
||||
export function buildPublishFileMap(projectDir: string): Map<string, Buffer> {
|
||||
const absProjectDir = resolve(projectDir);
|
||||
const filePaths: string[] = [];
|
||||
collectProjectFiles(absProjectDir, absProjectDir, filePaths);
|
||||
collectProjectFiles(absProjectDir, absProjectDir, filePaths, createProjectIgnore(absProjectDir));
|
||||
if (!filePaths.includes("index.html")) {
|
||||
throw new Error("Project must include an index.html file at the root before publish.");
|
||||
throw new Error(
|
||||
"Project archive must include index.html at the root. Check that .hyperframesignore does not exclude it.",
|
||||
);
|
||||
}
|
||||
|
||||
const fileContents = new Map<string, Buffer>();
|
||||
@@ -418,10 +439,10 @@ export function zipPublishFileMap(fileContents: Map<string, Buffer>): PublishArc
|
||||
|
||||
/**
|
||||
* Thin composition of `buildPublishFileMap` + `zipPublishFileMap` — signature
|
||||
* and behavior UNCHANGED from before the U6 split. `cloud render`
|
||||
* (`commands/cloud/render.ts`, `maybeUploadProject`) calls this directly and
|
||||
* must stay byte-identical (never see baked proxies); only `publish.ts` calls
|
||||
* the two halves separately with a baking transform in between.
|
||||
* and behavior UNCHANGED from before the U6 split. `cloud render` composes the
|
||||
* same two functions without an intermediate transform and must stay
|
||||
* byte-identical (never see baked proxies); only `publish.ts` inserts a baking
|
||||
* transform between them.
|
||||
*/
|
||||
export function createPublishArchive(projectDir: string): PublishArchiveResult {
|
||||
return zipPublishFileMap(buildPublishFileMap(projectDir));
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
* original untouched. On-disk project files are never modified — only the
|
||||
* in-memory archive file map passed in by `publish.ts` (built via
|
||||
* `buildPublishFileMap`, baked here, then zipped via `zipPublishFileMap`).
|
||||
* `cloud render` never calls this: it uses `createPublishArchive` directly,
|
||||
* which has no baking hook (R2 in the plan).
|
||||
* `cloud render` never calls this: it builds and zips the file map without an
|
||||
* intermediate baking transform (R2 in the plan).
|
||||
*
|
||||
* Alpha-bearing sources bake as VP9/WebM so transparency survives. A failed
|
||||
* hostile transcode aborts publish with a
|
||||
|
||||
Reference in New Issue
Block a user