mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-01 19:26:52 +00:00
* feat(cli): skipContrastInMotion moves to HF_TOOL_CONTEXT check-review
The --skip-contrast-in-motion flag and HYPERFRAMES_CHECK_SKIP_CONTRAST_IN_MOTION
env give way to a check-review capability in HF_TOOL_CONTEXT, so only a harness
turns it on. The skill text and manifest go back to main.
* feat(cli): check-review findingFrames saves one plain frame per finding time
With HF_TOOL_CONTEXT check-review.findingFrames, every error/warning finding in
layout or contrast with a real bbox gets frameTime (the middle sample of its
longest held run, by the heldMs gap rule) and frame (the absolute path of an
unannotated full frame shared by every finding at that time). One seek and
screenshot per distinct time, capped at 60 times with errors first, into the
run's snapshot folder; not listed in snapshots, history or events. Without the
capability, output is unchanged.
* docs(cli): finding-frame comments name the check-review capability, not a flag
* fix(cli): validate finding frame times and preserve cancellation
* fix(cli): count finding frame progress toward the check deadline
* feat(cli): check-review inspect attaches the inspector's report to each framed finding
With findingFrames on, the finding-frames page is served with the
inspector's GSAP hooks. After each frame is captured, every finding framed
at that time is inspected by its data-hf-id, else its selector, and the
inspector's own report is attached as the finding's inspect field. A failed
inspection becomes that finding's { error } and never fails the check.
Without the capability nothing changes.
Drops the unused @types/mime-types devDependency that the dependency audit
flags once packages/cli/package.json is touched.
* fix(cli): stop finding inspections after cancellation
* fix(cli): reject ambiguous finding inspection targets
* fix(ci): build inspector before Windows CLI smoke
* fix(cli): parse viewport HTML with explicit server parser
210 lines
8.1 KiB
JavaScript
210 lines
8.1 KiB
JavaScript
// Cross-platform replacement for the previous `mkdir -p … && cp -r …` shell
|
|
// chain, which failed on Windows because `cp` doesn't accept `-r` there.
|
|
|
|
import {
|
|
chmodSync,
|
|
copyFileSync,
|
|
cpSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
} from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const CLI_ROOT = resolve(HERE, "..");
|
|
const REPO_ROOT = resolve(CLI_ROOT, "..", "..");
|
|
const DIST = join(CLI_ROOT, "dist");
|
|
|
|
// Studio's vite build clears its dist before rewriting it; don't start the
|
|
// copy until both sentinels are present so we never observe a partial tree.
|
|
const STUDIO_WAIT_TIMEOUT_MS = 30_000;
|
|
const STUDIO_POLL_INTERVAL_MS = 250;
|
|
|
|
// fallow-ignore-next-line complexity
|
|
async function waitForStudioDist(dir) {
|
|
const deadline = Date.now() + STUDIO_WAIT_TIMEOUT_MS;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const entries = new Set(readdirSync(dir));
|
|
// vite emits `assets/` before rewriting `index.html` at the end of the
|
|
// build — so once both are present, the tree is complete.
|
|
if (entries.has("index.html") && entries.has("assets")) return;
|
|
} catch {
|
|
// dir doesn't exist yet — vite will create it
|
|
}
|
|
await sleep(STUDIO_POLL_INTERVAL_MS);
|
|
}
|
|
throw new Error(`[build-copy] timed out waiting for studio dist at ${dir}`);
|
|
}
|
|
|
|
function copyDir(src, dest) {
|
|
cpSync(src, dest, { recursive: true, force: true });
|
|
}
|
|
|
|
// Source maps stay out: the CLI ships none of its own (tsup sourcemap: false).
|
|
function copyDirContents(src, dest) {
|
|
for (const entry of readdirSync(src)) {
|
|
cpSync(join(src, entry), join(dest, entry), {
|
|
recursive: true,
|
|
force: true,
|
|
filter: (path) => !path.endsWith(".map"),
|
|
});
|
|
}
|
|
}
|
|
|
|
function copyMdFiles(srcDir, destDir) {
|
|
if (!existsSync(srcDir)) return;
|
|
for (const name of readdirSync(srcDir)) {
|
|
if (name.endsWith(".md")) {
|
|
cpSync(join(srcDir, name), join(destDir, name));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Studio preview loads these from the CLI's own server instead of jsDelivr
|
|
// (studioServer.ts serves dist/vendor/npm). They come from the workspace's
|
|
// installed gsap, which must match the version core asks for, or Studio would
|
|
// quietly fall back to the CDN for every preview.
|
|
const GSAP_VENDOR_FILES = ["gsap.min.js", "CustomEase.min.js", "MotionPathPlugin.min.js"];
|
|
|
|
function copyGsapVendorFiles() {
|
|
const coreRequire = createRequire(join(REPO_ROOT, "packages", "core", "package.json"));
|
|
const gsapRoot = dirname(coreRequire.resolve("gsap/package.json"));
|
|
const { version } = JSON.parse(readFileSync(join(gsapRoot, "package.json"), "utf-8"));
|
|
const coreSource = readFileSync(
|
|
join(REPO_ROOT, "packages", "core", "src", "gsapCdn.ts"),
|
|
"utf-8",
|
|
);
|
|
const expected = coreSource.match(/GSAP_CDN_VERSION = "([^"]+)"/)?.[1];
|
|
if (version !== expected) {
|
|
throw new Error(
|
|
`[build-copy] installed gsap ${version} does not match GSAP_CDN_VERSION ${expected}`,
|
|
);
|
|
}
|
|
const vendorDir = join(DIST, "vendor", "npm");
|
|
rmSync(vendorDir, { recursive: true, force: true });
|
|
const dest = join(vendorDir, `gsap@${version}`, "dist");
|
|
mkdirSync(dest, { recursive: true });
|
|
for (const file of GSAP_VENDOR_FILES) {
|
|
copyFileSync(join(gsapRoot, "dist", file), join(dest, file));
|
|
chmodSync(join(dest, file), 0o644);
|
|
}
|
|
}
|
|
|
|
// fallow-ignore-next-line complexity
|
|
async function main() {
|
|
for (const sub of ["studio", "docs", "templates", "skills", "docker"]) {
|
|
mkdirSync(join(DIST, sub), { recursive: true });
|
|
}
|
|
mkdirSync(join(DIST, "commands"), { recursive: true });
|
|
|
|
const studioDist = resolve(CLI_ROOT, "..", "studio", "dist");
|
|
await waitForStudioDist(studioDist);
|
|
// Only the SPA studioServer.ts serves (index.html, /assets, /icons, /favicon.svg);
|
|
// the rest of studio/dist is the npm library build.
|
|
for (const entry of ["index.html", "assets", "icons", "favicon.svg"]) {
|
|
copyDir(join(studioDist, entry), join(DIST, "studio", entry));
|
|
}
|
|
|
|
for (const tmpl of ["blank", "from-file", "_shared"]) {
|
|
copyDir(join(CLI_ROOT, "src", "templates", tmpl), join(DIST, "templates", tmpl));
|
|
}
|
|
|
|
// Bundle warm-grain from the repo registry so the built CLI can scaffold it
|
|
// offline and CI smoke tests pick up PR-branch changes before merge to main.
|
|
const warmGrainSrc = join(REPO_ROOT, "registry", "examples", "warm-grain");
|
|
if (existsSync(warmGrainSrc)) {
|
|
copyDir(warmGrainSrc, join(DIST, "templates", "warm-grain"));
|
|
}
|
|
|
|
// Skills bundled into the published CLI. Branches don't all carry the same
|
|
// skills/ tree (it gets restructured), so each entry is existsSync-guarded:
|
|
// a missing skill dir warns + skips instead of crashing the build.
|
|
for (const skill of ["hyperframes", "hyperframes-cli", "gsap", "media-use"]) {
|
|
const src = join(REPO_ROOT, "skills", skill);
|
|
if (!existsSync(src)) {
|
|
console.warn(`[build-copy] skill not found, skipping: skills/${skill}`);
|
|
continue;
|
|
}
|
|
const destination = join(DIST, "skills", skill);
|
|
rmSync(destination, { recursive: true, force: true });
|
|
copyDir(src, destination);
|
|
}
|
|
|
|
// The media-use engine lives with the CLI source, but keeps its published
|
|
// skill-relative layout so the moved .mjs tree can run without a rewrite.
|
|
const mediaEngine = join(CLI_ROOT, "src", "media-use");
|
|
const publishedMediaLib = join(DIST, "skills", "media-use", "scripts", "lib");
|
|
rmSync(publishedMediaLib, { recursive: true, force: true });
|
|
mkdirSync(publishedMediaLib, { recursive: true });
|
|
copyDirContents(join(mediaEngine, "lib"), publishedMediaLib);
|
|
cpSync(
|
|
join(mediaEngine, "resolve.mjs"),
|
|
join(DIST, "skills", "media-use", "scripts", "resolve.mjs"),
|
|
);
|
|
|
|
const dockerfile = join(CLI_ROOT, "src", "docker", "Dockerfile.render");
|
|
if (existsSync(dockerfile)) {
|
|
cpSync(dockerfile, join(DIST, "docker", "Dockerfile.render"));
|
|
}
|
|
|
|
const layoutAuditScript = join(CLI_ROOT, "src", "commands", "layout-audit.browser.js");
|
|
if (existsSync(layoutAuditScript)) {
|
|
cpSync(layoutAuditScript, join(DIST, "commands", "layout-audit.browser.js"));
|
|
}
|
|
|
|
const contrastAuditScript = join(CLI_ROOT, "src", "commands", "contrast-audit.browser.js");
|
|
if (existsSync(contrastAuditScript)) {
|
|
cpSync(contrastAuditScript, join(DIST, "commands", "contrast-audit.browser.js"));
|
|
}
|
|
|
|
const motionSignatureScript = join(CLI_ROOT, "src", "commands", "motion-signature.browser.js");
|
|
if (existsSync(motionSignatureScript)) {
|
|
cpSync(motionSignatureScript, join(DIST, "commands", "motion-signature.browser.js"));
|
|
}
|
|
|
|
const motionSampleScript = join(CLI_ROOT, "src", "commands", "motion-sample.browser.js");
|
|
if (existsSync(motionSampleScript)) {
|
|
cpSync(motionSampleScript, join(DIST, "commands", "motion-sample.browser.js"));
|
|
}
|
|
|
|
// Player bundles for the standalone browser player used by `present` and
|
|
// `play`. resolvePlayerPath/resolveSlideshowPath look for these alongside the
|
|
// built CLI (dist/<name>.global.js), so they must ship in the package — the
|
|
// monorepo-dev fallback paths don't exist once installed from npm. Without
|
|
// this, `npx hyperframes present` fails with "@hyperframes/player not found".
|
|
const playerDist = join(REPO_ROOT, "packages", "player", "dist");
|
|
const playerGlobals = [
|
|
[join(playerDist, "hyperframes-player.global.js"), join(DIST, "hyperframes-player.global.js")],
|
|
[
|
|
join(playerDist, "slideshow", "hyperframes-slideshow.global.js"),
|
|
join(DIST, "hyperframes-slideshow.global.js"),
|
|
],
|
|
];
|
|
for (const [src, dest] of playerGlobals) {
|
|
if (existsSync(src)) {
|
|
cpSync(src, dest);
|
|
} else {
|
|
console.warn(`[build-copy] player bundle not found, skipping: ${src}`);
|
|
}
|
|
}
|
|
|
|
// The bundled inspector reads its page runtime from beside its own chunk.
|
|
const inspectorRuntime = join(REPO_ROOT, "packages", "inspector", "dist", "inspection.js");
|
|
cpSync(inspectorRuntime, join(DIST, "inspection.js"));
|
|
|
|
copyGsapVendorFiles();
|
|
|
|
copyMdFiles(join(CLI_ROOT, "src", "docs"), join(DIST, "docs"));
|
|
|
|
console.log("[build-copy] done");
|
|
}
|
|
|
|
await main();
|