mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(skills): bundle modular capture helpers (#2456)
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
"files": 1
|
||||
},
|
||||
"hyperframes-animation": {
|
||||
"hash": "40a79102f708b4f5",
|
||||
"hash": "70705a50c3adea48",
|
||||
"files": 102
|
||||
},
|
||||
"hyperframes-cli": {
|
||||
@@ -34,7 +34,7 @@
|
||||
"files": 14
|
||||
},
|
||||
"hyperframes-creative": {
|
||||
"hash": "a508ecd60aaa6dbf",
|
||||
"hash": "ffe13c76b89ce3d1",
|
||||
"files": 78
|
||||
},
|
||||
"hyperframes-keyframes": {
|
||||
|
||||
@@ -17,14 +17,21 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { resolve, join } from "node:path";
|
||||
import { sampleTweenBboxes } from "./animation-map-sampling.mjs";
|
||||
import { hyperframesPackageSpec, importPackagesOrBootstrap } from "./package-loader.mjs";
|
||||
import {
|
||||
bundleCompositionForCapture,
|
||||
hyperframesPackageSpec,
|
||||
importPackagesOrBootstrap,
|
||||
} from "./package-loader.mjs";
|
||||
|
||||
const packages = await importPackagesOrBootstrap(["@hyperframes/producer", "@hyperframes/core"], {
|
||||
npmPackages: [
|
||||
hyperframesPackageSpec("@hyperframes/producer"),
|
||||
hyperframesPackageSpec("@hyperframes/core"),
|
||||
],
|
||||
});
|
||||
const packages = await importPackagesOrBootstrap(
|
||||
["@hyperframes/producer", "@hyperframes/core", "@hyperframes/core/compiler"],
|
||||
{
|
||||
npmPackages: [
|
||||
hyperframesPackageSpec("@hyperframes/producer"),
|
||||
hyperframesPackageSpec("@hyperframes/core"),
|
||||
],
|
||||
},
|
||||
);
|
||||
const {
|
||||
createFileServer,
|
||||
createCaptureSession,
|
||||
@@ -53,16 +60,25 @@ await mkdir(OUT_DIR, { recursive: true });
|
||||
|
||||
// ─── Main ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const server = await createFileServer({ projectDir: COMP_DIR, port: 0 });
|
||||
const session = await createCaptureSession(
|
||||
server.url,
|
||||
OUT_DIR,
|
||||
{ width: WIDTH, height: HEIGHT, fps: FPS, format: "png" },
|
||||
null,
|
||||
);
|
||||
await initializeSession(session);
|
||||
|
||||
// Raw modular hosts do not mount child compositions in the capture helper.
|
||||
// Bundle first so duration/timeline discovery sees the same DOM as render/check.
|
||||
const bundle = await bundleCompositionForCapture(packages["@hyperframes/core/compiler"], COMP_DIR);
|
||||
let server;
|
||||
let session;
|
||||
try {
|
||||
server = await createFileServer({
|
||||
projectDir: COMP_DIR,
|
||||
compiledDir: bundle.compiledDir,
|
||||
port: 0,
|
||||
});
|
||||
session = await createCaptureSession(
|
||||
server.url,
|
||||
OUT_DIR,
|
||||
{ width: WIDTH, height: HEIGHT, fps: FPS, format: "png" },
|
||||
null,
|
||||
);
|
||||
await initializeSession(session);
|
||||
|
||||
const duration = await getCompositionDuration(session);
|
||||
const tweens = await enumerateTweens(session);
|
||||
const kept = tweens.filter((tw) => tw.end - tw.start >= MIN_DUR);
|
||||
@@ -126,8 +142,9 @@ try {
|
||||
|
||||
printSummary(report);
|
||||
} finally {
|
||||
await closeCaptureSession(session).catch(() => {});
|
||||
server.close();
|
||||
if (session) await closeCaptureSession(session).catch(() => {});
|
||||
server?.close();
|
||||
bundle.cleanup();
|
||||
}
|
||||
|
||||
// ─── Seek helper ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -14,7 +14,7 @@ const HELPERS = [
|
||||
|
||||
describe("HyperFrames skill helpers", () => {
|
||||
for (const helper of HELPERS)
|
||||
it(`${helper.split("/").at(-1)} uses canonical rational frame-rate parsing`, () => {
|
||||
it(`${helper.split("/").at(-1)} bundles modular input and uses rational fps`, () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "hyperframes-skill-helper-test-"));
|
||||
const packageDir = join(root, "node_modules", "@hyperframes", "producer");
|
||||
const corePackageDir = join(root, "node_modules", "@hyperframes", "core");
|
||||
@@ -31,7 +31,15 @@ describe("HyperFrames skill helpers", () => {
|
||||
writeFileSync(
|
||||
join(packageDir, "index.mjs"),
|
||||
[
|
||||
'export async function createFileServer() { return { url: "http://test", close() {} }; }',
|
||||
'import { readFileSync } from "node:fs";',
|
||||
'import { join } from "node:path";',
|
||||
"export async function createFileServer(options) {",
|
||||
' const bundled = readFileSync(join(options.compiledDir, "index.html"), "utf8");',
|
||||
' if (bundled !== "<!doctype html><main>bundled modular composition</main>") {',
|
||||
" throw new Error(`UNEXPECTED_BUNDLE=${bundled}`);",
|
||||
" }",
|
||||
' return { url: "http://test", close() {} };',
|
||||
"}",
|
||||
"export async function createCaptureSession(_url, _out, options) {",
|
||||
" throw new Error(`CAPTURE_OPTIONS=${JSON.stringify(options)}`);",
|
||||
"}",
|
||||
@@ -42,7 +50,11 @@ describe("HyperFrames skill helpers", () => {
|
||||
);
|
||||
writeFileSync(
|
||||
join(corePackageDir, "package.json"),
|
||||
JSON.stringify({ name: "@hyperframes/core", type: "module", exports: "./index.mjs" }),
|
||||
JSON.stringify({
|
||||
name: "@hyperframes/core",
|
||||
type: "module",
|
||||
exports: { ".": "./index.mjs", "./compiler": "./compiler.mjs" },
|
||||
}),
|
||||
);
|
||||
writeFileSync(
|
||||
join(corePackageDir, "index.mjs"),
|
||||
@@ -54,6 +66,14 @@ describe("HyperFrames skill helpers", () => {
|
||||
"}",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
join(corePackageDir, "compiler.mjs"),
|
||||
[
|
||||
"export async function bundleToSingleHtml() {",
|
||||
' return "<!doctype html><main>bundled modular composition</main>";',
|
||||
"}",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
join(sharpPackageDir, "package.json"),
|
||||
JSON.stringify({ name: "sharp", type: "module", exports: "./index.mjs" }),
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// The `installLine` strings below are DISPLAY ONLY (shown in the prompt / error
|
||||
// text); they are never handed to a shell or executed.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, delimiter, dirname, join, parse, resolve } from "node:path";
|
||||
@@ -56,6 +56,23 @@ export async function importPackagesOrBootstrap(packageNames, options = {}) {
|
||||
return modules;
|
||||
}
|
||||
|
||||
export async function bundleCompositionForCapture(compiler, projectDir) {
|
||||
const compiledDir = mkdtempSync(join(tmpdir(), "hyperframes-skill-bundle-"));
|
||||
try {
|
||||
const html = await compiler.bundleToSingleHtml(projectDir);
|
||||
writeFileSync(join(compiledDir, "index.html"), html);
|
||||
return {
|
||||
compiledDir,
|
||||
cleanup() {
|
||||
rmSync(compiledDir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
rmSync(compiledDir, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function hyperframesPackageSpec(packageName) {
|
||||
const override = process.env[VERSION_OVERRIDE_ENV]?.trim();
|
||||
if (override) return `${packageName}@${override}`;
|
||||
@@ -79,6 +96,7 @@ export function hyperframesPackageSpec(packageName) {
|
||||
|
||||
function resolvePackageEntry(packageName) {
|
||||
const bases = [process.cwd(), HERE, ...envNodeModulesDirs(), ...nodeModulesDirsFromPath()];
|
||||
const { rootName, subpath } = splitPackageSpecifier(packageName);
|
||||
|
||||
const seen = new Set();
|
||||
for (const base of bases) {
|
||||
@@ -91,8 +109,8 @@ function resolvePackageEntry(packageName) {
|
||||
packageName,
|
||||
);
|
||||
} catch {
|
||||
const packageDir = findPackageDir(normalized, packageName);
|
||||
const packageEntry = packageDir ? readPackageEntry(packageDir) : null;
|
||||
const packageDir = findPackageDir(normalized, rootName);
|
||||
const packageEntry = packageDir ? readPackageEntry(packageDir, subpath) : null;
|
||||
if (packageEntry) return packageEntry;
|
||||
}
|
||||
}
|
||||
@@ -100,6 +118,15 @@ function resolvePackageEntry(packageName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function splitPackageSpecifier(packageName) {
|
||||
const segments = packageName.split("/");
|
||||
const rootLength = packageName.startsWith("@") ? 2 : 1;
|
||||
return {
|
||||
rootName: segments.slice(0, rootLength).join("/"),
|
||||
subpath: segments.slice(rootLength).join("/"),
|
||||
};
|
||||
}
|
||||
|
||||
function readBundledHyperframesVersion() {
|
||||
for (const ancestor of ancestors(HERE)) {
|
||||
const directVersion = readPackageVersion(join(ancestor, "package.json"));
|
||||
@@ -152,10 +179,14 @@ function findPackageDir(base, packageName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPackageEntry(packageDir) {
|
||||
function readPackageEntry(packageDir, subpath = "") {
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
||||
const entry = exportEntry(manifest.exports) ?? manifest.module ?? manifest.main ?? "index.js";
|
||||
const requestedExport = subpath ? manifest.exports?.[`./${subpath}`] : manifest.exports;
|
||||
const entry =
|
||||
exportEntry(requestedExport) ??
|
||||
(!subpath ? (manifest.module ?? manifest.main ?? "index.js") : null);
|
||||
if (!entry) return null;
|
||||
const entryPath = join(packageDir, entry);
|
||||
return existsSync(entryPath) ? entryPath : null;
|
||||
} catch {
|
||||
|
||||
@@ -45,12 +45,16 @@
|
||||
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { hyperframesPackageSpec, importPackagesOrBootstrap } from "./package-loader.mjs";
|
||||
import {
|
||||
bundleCompositionForCapture,
|
||||
hyperframesPackageSpec,
|
||||
importPackagesOrBootstrap,
|
||||
} from "./package-loader.mjs";
|
||||
|
||||
// Use the producer's file server — it auto-injects the HyperFrames runtime
|
||||
// and render-seek bridge, so raw authoring HTML works without a build step.
|
||||
// Bundle first so mounted sub-compositions are inlined before the producer's
|
||||
// file server injects the HyperFrames runtime and render-seek bridge.
|
||||
const packages = await importPackagesOrBootstrap(
|
||||
["@hyperframes/producer", "@hyperframes/core", "sharp"],
|
||||
["@hyperframes/producer", "@hyperframes/core", "@hyperframes/core/compiler", "sharp"],
|
||||
{
|
||||
npmPackages: [
|
||||
hyperframesPackageSpec("@hyperframes/producer"),
|
||||
@@ -88,16 +92,23 @@ const COMP_DIR = resolve(args.composition);
|
||||
|
||||
await mkdir(OUT_DIR, { recursive: true });
|
||||
|
||||
const server = await createFileServer({ projectDir: COMP_DIR, port: 0 });
|
||||
const session = await createCaptureSession(
|
||||
server.url,
|
||||
OUT_DIR,
|
||||
{ width: WIDTH, height: HEIGHT, fps: FPS, format: "png" },
|
||||
null,
|
||||
);
|
||||
await initializeSession(session);
|
||||
|
||||
const bundle = await bundleCompositionForCapture(packages["@hyperframes/core/compiler"], COMP_DIR);
|
||||
let server;
|
||||
let session;
|
||||
try {
|
||||
server = await createFileServer({
|
||||
projectDir: COMP_DIR,
|
||||
compiledDir: bundle.compiledDir,
|
||||
port: 0,
|
||||
});
|
||||
session = await createCaptureSession(
|
||||
server.url,
|
||||
OUT_DIR,
|
||||
{ width: WIDTH, height: HEIGHT, fps: FPS, format: "png" },
|
||||
null,
|
||||
);
|
||||
await initializeSession(session);
|
||||
|
||||
const duration = await getCompositionDuration(session);
|
||||
const times = Array.from(
|
||||
{ length: SAMPLES },
|
||||
@@ -145,8 +156,9 @@ try {
|
||||
printSummary(report);
|
||||
process.exitCode = report.summary.failAA > 0 ? 1 : 0;
|
||||
} finally {
|
||||
await closeCaptureSession(session).catch(() => {});
|
||||
server.close();
|
||||
if (session) await closeCaptureSession(session).catch(() => {});
|
||||
server?.close();
|
||||
bundle.cleanup();
|
||||
}
|
||||
|
||||
// ─── DOM probe + text-hide (runs in the page) ────────────────────────────────
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// The `installLine` strings below are DISPLAY ONLY (shown in the prompt / error
|
||||
// text); they are never handed to a shell or executed.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, delimiter, dirname, join, parse, resolve } from "node:path";
|
||||
@@ -56,6 +56,23 @@ export async function importPackagesOrBootstrap(packageNames, options = {}) {
|
||||
return modules;
|
||||
}
|
||||
|
||||
export async function bundleCompositionForCapture(compiler, projectDir) {
|
||||
const compiledDir = mkdtempSync(join(tmpdir(), "hyperframes-skill-bundle-"));
|
||||
try {
|
||||
const html = await compiler.bundleToSingleHtml(projectDir);
|
||||
writeFileSync(join(compiledDir, "index.html"), html);
|
||||
return {
|
||||
compiledDir,
|
||||
cleanup() {
|
||||
rmSync(compiledDir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
rmSync(compiledDir, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function hyperframesPackageSpec(packageName) {
|
||||
const override = process.env[VERSION_OVERRIDE_ENV]?.trim();
|
||||
if (override) return `${packageName}@${override}`;
|
||||
@@ -79,6 +96,7 @@ export function hyperframesPackageSpec(packageName) {
|
||||
|
||||
function resolvePackageEntry(packageName) {
|
||||
const bases = [process.cwd(), HERE, ...envNodeModulesDirs(), ...nodeModulesDirsFromPath()];
|
||||
const { rootName, subpath } = splitPackageSpecifier(packageName);
|
||||
|
||||
const seen = new Set();
|
||||
for (const base of bases) {
|
||||
@@ -91,8 +109,8 @@ function resolvePackageEntry(packageName) {
|
||||
packageName,
|
||||
);
|
||||
} catch {
|
||||
const packageDir = findPackageDir(normalized, packageName);
|
||||
const packageEntry = packageDir ? readPackageEntry(packageDir) : null;
|
||||
const packageDir = findPackageDir(normalized, rootName);
|
||||
const packageEntry = packageDir ? readPackageEntry(packageDir, subpath) : null;
|
||||
if (packageEntry) return packageEntry;
|
||||
}
|
||||
}
|
||||
@@ -100,6 +118,15 @@ function resolvePackageEntry(packageName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function splitPackageSpecifier(packageName) {
|
||||
const segments = packageName.split("/");
|
||||
const rootLength = packageName.startsWith("@") ? 2 : 1;
|
||||
return {
|
||||
rootName: segments.slice(0, rootLength).join("/"),
|
||||
subpath: segments.slice(rootLength).join("/"),
|
||||
};
|
||||
}
|
||||
|
||||
function readBundledHyperframesVersion() {
|
||||
for (const ancestor of ancestors(HERE)) {
|
||||
const directVersion = readPackageVersion(join(ancestor, "package.json"));
|
||||
@@ -152,10 +179,14 @@ function findPackageDir(base, packageName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPackageEntry(packageDir) {
|
||||
function readPackageEntry(packageDir, subpath = "") {
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
||||
const entry = exportEntry(manifest.exports) ?? manifest.module ?? manifest.main ?? "index.js";
|
||||
const requestedExport = subpath ? manifest.exports?.[`./${subpath}`] : manifest.exports;
|
||||
const entry =
|
||||
exportEntry(requestedExport) ??
|
||||
(!subpath ? (manifest.module ?? manifest.main ?? "index.js") : null);
|
||||
if (!entry) return null;
|
||||
const entryPath = join(packageDir, entry);
|
||||
return existsSync(entryPath) ? entryPath : null;
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user