fix(skills): bundle modular capture helpers (#2456)

This commit is contained in:
Miguel Ángel
2026-07-14 20:35:05 -04:00
committed by GitHub
parent ada878fdcd
commit 15ca6fd129
6 changed files with 159 additions and 48 deletions
+2 -2
View File
@@ -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 {