mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
ci: verify on windows-latest + fix cross-platform build bugs it surfaced (#342)
* fix(cli): make build copy cross-platform and deterministic
* fix(core): keep rewritten asset URLs POSIX on Windows
* ci(windows): add render verification workflow
* ci(windows): load canary gsap from cdn
* build: use dependency-aware workspace ordering
* Revert "build: use dependency-aware workspace ordering"
This reverts commit 99bc2ffbdf.
This commit is contained in:
@@ -17,11 +17,10 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"dev": "tsx src/cli.ts",
|
||||
"build": "bun run build:fonts && bun run build:studio && tsup && bun run build:runtime && bun run build:copy",
|
||||
"build": "bun run build:fonts && tsup && bun run build:runtime && bun run build:copy",
|
||||
"build:fonts": "cd ../producer && tsx scripts/generate-font-data.ts",
|
||||
"build:studio": "cd ../studio && bun run build",
|
||||
"build:runtime": "tsx scripts/build-runtime.ts",
|
||||
"build:copy": "mkdir -p dist/studio dist/docs dist/templates dist/skills dist/docker && cp -r ../studio/dist/* dist/studio/ && cp -r src/templates/blank src/templates/_shared dist/templates/ && cp -r ../../skills/hyperframes ../../skills/hyperframes-cli ../../skills/gsap dist/skills/ && cp src/docker/Dockerfile.render dist/docker/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true)",
|
||||
"build:copy": "node scripts/build-copy.mjs",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -44,6 +43,7 @@
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/engine": "workspace:*",
|
||||
"@hyperframes/producer": "workspace:*",
|
||||
"@hyperframes/studio": "workspace:*",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/node": "^22.0.0",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Cross-platform replacement for the previous `mkdir -p … && cp -r …` shell
|
||||
// chain, which failed on Windows because `cp` doesn't accept `-r` there.
|
||||
|
||||
import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
||||
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;
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
function copyDirContents(src, dest) {
|
||||
for (const entry of readdirSync(src)) {
|
||||
cpSync(join(src, entry), join(dest, entry), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
for (const sub of ["studio", "docs", "templates", "skills", "docker"]) {
|
||||
mkdirSync(join(DIST, sub), { recursive: true });
|
||||
}
|
||||
|
||||
const studioDist = resolve(CLI_ROOT, "..", "studio", "dist");
|
||||
await waitForStudioDist(studioDist);
|
||||
copyDirContents(studioDist, join(DIST, "studio"));
|
||||
|
||||
for (const tmpl of ["blank", "_shared"]) {
|
||||
copyDir(join(CLI_ROOT, "src", "templates", tmpl), join(DIST, "templates", tmpl));
|
||||
}
|
||||
|
||||
for (const skill of ["hyperframes", "hyperframes-cli", "gsap"]) {
|
||||
copyDir(join(REPO_ROOT, "skills", skill), join(DIST, "skills", skill));
|
||||
}
|
||||
|
||||
const dockerfile = join(CLI_ROOT, "src", "docker", "Dockerfile.render");
|
||||
if (existsSync(dockerfile)) {
|
||||
cpSync(dockerfile, join(DIST, "docker", "Dockerfile.render"));
|
||||
}
|
||||
|
||||
copyMdFiles(join(CLI_ROOT, "src", "docs"), join(DIST, "docs"));
|
||||
|
||||
console.log("[build-copy] done");
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { rewriteAssetPath, rewriteCssAssetUrls } from "./rewriteSubCompPaths.js";
|
||||
|
||||
describe("rewriteAssetPath", () => {
|
||||
it("rewrites `../` against the sub-composition dir", () => {
|
||||
expect(rewriteAssetPath("compositions/scene.html", "../icon.svg")).toBe("icon.svg");
|
||||
});
|
||||
|
||||
it("leaves plain relative paths untouched", () => {
|
||||
expect(rewriteAssetPath("compositions/scene.html", "assets/logo.png")).toBe("assets/logo.png");
|
||||
});
|
||||
|
||||
it("leaves absolute URLs and data URIs untouched", () => {
|
||||
expect(rewriteAssetPath("compositions/scene.html", "https://x/y")).toBe("https://x/y");
|
||||
expect(rewriteAssetPath("compositions/scene.html", "data:image/png;base64,AA")).toBe(
|
||||
"data:image/png;base64,AA",
|
||||
);
|
||||
expect(rewriteAssetPath("compositions/scene.html", "#hash")).toBe("#hash");
|
||||
});
|
||||
|
||||
// Regression guard for a Windows-only bug: the rewriter used to import
|
||||
// `path` (native) and emit `:\fonts\brand.woff2` — native `join` used
|
||||
// backslashes, and `resolve("/", x).slice(1)` chopped the `D` off a
|
||||
// `D:\…` absolute path. URLs must be POSIX regardless of host OS.
|
||||
it("never emits backslashes on any platform", () => {
|
||||
const out = rewriteAssetPath("compositions/nested/scene.html", "../../fonts/brand.woff2");
|
||||
expect(out).toBe("fonts/brand.woff2");
|
||||
expect(out).not.toMatch(/\\/);
|
||||
expect(out).not.toMatch(/^:/);
|
||||
});
|
||||
|
||||
it("CSS url(...) rewrites also stay POSIX under nesting", () => {
|
||||
const css = `@font-face { src: url("../../fonts/brand.woff2") format("woff2"); }`;
|
||||
const out = rewriteCssAssetUrls(css, "compositions/nested/scene.html");
|
||||
expect(out).toContain(`url("fonts/brand.woff2")`);
|
||||
expect(out).not.toMatch(/\\/);
|
||||
expect(out).not.toMatch(/:\\/);
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,11 @@
|
||||
* to ensure consistent behavior.
|
||||
*/
|
||||
|
||||
import { join, resolve, dirname } from "path";
|
||||
// URL paths in HTML output are POSIX regardless of host OS — use the `posix`
|
||||
// submodule so Windows builds don't emit backslash-separated paths (or worse,
|
||||
// drive-letter-prefixed artifacts from `resolve("/", ...)`).
|
||||
import { posix } from "path";
|
||||
const { join, resolve, dirname } = posix;
|
||||
|
||||
/** Attributes that may contain relative asset paths. */
|
||||
const PATH_ATTRS = ["src", "href"] as const;
|
||||
|
||||
Reference in New Issue
Block a user