mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +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:
@@ -0,0 +1,81 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #f4eee4;
|
||||||
|
font-family:
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
"Segoe UI",
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
#root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
#headline {
|
||||||
|
font-size: 180px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 0.9;
|
||||||
|
color: #c7c4f7;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#sub {
|
||||||
|
font-size: 36px;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #f4eee4;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
#bar {
|
||||||
|
width: 560px;
|
||||||
|
height: 4px;
|
||||||
|
background: #9b95f0;
|
||||||
|
transform-origin: left center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div
|
||||||
|
id="root"
|
||||||
|
data-composition-id="main"
|
||||||
|
data-start="0"
|
||||||
|
data-duration="8"
|
||||||
|
data-width="1920"
|
||||||
|
data-height="1080"
|
||||||
|
>
|
||||||
|
<div id="headline">HYPERFRAMES</div>
|
||||||
|
<div id="sub">RENDERED ON WINDOWS</div>
|
||||||
|
<div id="bar"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
window.__timelines = window.__timelines || {};
|
||||||
|
const tl = gsap.timeline({ paused: true });
|
||||||
|
tl.from("#headline", { y: 80, opacity: 0, duration: 0.9, ease: "power3.out" }, 0.2);
|
||||||
|
tl.from("#sub", { y: 40, opacity: 0, duration: 0.7, ease: "power2.out" }, 0.6);
|
||||||
|
tl.fromTo("#bar", { scaleX: 0 }, { scaleX: 1, duration: 1.2, ease: "power4.inOut" }, 1.0);
|
||||||
|
tl.to("#headline", { letterSpacing: "-0.01em", duration: 4.0, ease: "sine.inOut" }, 2.5);
|
||||||
|
tl.to(["#headline", "#sub", "#bar"], { opacity: 0, duration: 0.8, ease: "power2.in" }, 7.0);
|
||||||
|
window.__timelines["main"] = tl;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
name: Windows render verification
|
||||||
|
|
||||||
|
# Manually triggered smoke test that renders a HyperFrames composition on a
|
||||||
|
# real Windows runner. Proves the PR #336 `where ffmpeg` fix actually works
|
||||||
|
# end-to-end: FFmpeg is discovered natively on Windows, Chrome is installed
|
||||||
|
# and launched, frames are captured, and an MP4 is produced — without Docker
|
||||||
|
# or WSL.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: "Git ref to render (branch / tag / SHA)."
|
||||||
|
required: false
|
||||||
|
default: "main"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: windows-render-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
changes:
|
||||||
|
name: Detect changes
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 2
|
||||||
|
outputs:
|
||||||
|
code: ${{ steps.filter.outputs.code }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dorny/paths-filter@v3
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
code:
|
||||||
|
- "packages/**"
|
||||||
|
- "scripts/**"
|
||||||
|
- "package.json"
|
||||||
|
- "bun.lock"
|
||||||
|
- ".github/workflows/windows-render.yml"
|
||||||
|
|
||||||
|
render-windows:
|
||||||
|
name: Render on windows-latest
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.code == 'true' || github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.ref }}
|
||||||
|
|
||||||
|
- name: Show platform info
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Write-Host "OS: $([System.Environment]::OSVersion.VersionString)"
|
||||||
|
Write-Host "PowerShell: $($PSVersionTable.PSVersion)"
|
||||||
|
Write-Host "Runner: windows-latest"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Install FFmpeg via Chocolatey (mirrors the recommended path for
|
||||||
|
# real Windows users — no Docker, no WSL).
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
- name: Install FFmpeg (Chocolatey)
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
choco install ffmpeg -y --no-progress
|
||||||
|
refreshenv
|
||||||
|
Write-Host "--- ffmpeg sanity check ---"
|
||||||
|
where.exe ffmpeg
|
||||||
|
ffmpeg -version | Select-Object -First 1
|
||||||
|
|
||||||
|
- name: Install Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
shell: pwsh
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build all packages
|
||||||
|
shell: pwsh
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Prove the PR #336 fix: hyperframes doctor exercises findFFmpeg()
|
||||||
|
# and whichBinary() — both must pass on Windows without workarounds.
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
- name: hyperframes doctor (verifies `where ffmpeg` fix)
|
||||||
|
shell: pwsh
|
||||||
|
run: node packages/cli/dist/cli.js doctor
|
||||||
|
|
||||||
|
- name: Scaffold canary composition
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
New-Item -ItemType Directory -Force -Path "$env:RUNNER_TEMP\windows-canary" | Out-Null
|
||||||
|
cd "$env:RUNNER_TEMP\windows-canary"
|
||||||
|
node "$env:GITHUB_WORKSPACE\packages\cli\dist\cli.js" init canary --example blank --non-interactive --skip-skills
|
||||||
|
|
||||||
|
$fixtures = "$env:GITHUB_WORKSPACE\.github\workflows\fixtures"
|
||||||
|
Copy-Item "$fixtures\windows-canary.html" "canary\index.html" -Force
|
||||||
|
|
||||||
|
- name: Render canary composition
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
cd "$env:RUNNER_TEMP\windows-canary\canary"
|
||||||
|
node "$env:GITHUB_WORKSPACE\packages\cli\dist\cli.js" render `
|
||||||
|
--fps 30 `
|
||||||
|
--quality draft `
|
||||||
|
--workers 2 `
|
||||||
|
--output renders\canary.mp4
|
||||||
|
|
||||||
|
- name: Verify rendered MP4
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$mp4 = "$env:RUNNER_TEMP\windows-canary\canary\renders\canary.mp4"
|
||||||
|
if (-not (Test-Path $mp4)) { throw "canary.mp4 not produced" }
|
||||||
|
|
||||||
|
$probe = ffprobe -v error -select_streams v:0 `
|
||||||
|
-show_entries stream=width,height,r_frame_rate -show_entries format=duration `
|
||||||
|
-of default=noprint_wrappers=1 $mp4
|
||||||
|
Write-Host $probe
|
||||||
|
|
||||||
|
# Parse probe output
|
||||||
|
$width = ($probe | Select-String '^width=(.+)$').Matches.Groups[1].Value
|
||||||
|
$height = ($probe | Select-String '^height=(.+)$').Matches.Groups[1].Value
|
||||||
|
$fps = ($probe | Select-String '^r_frame_rate=(.+)$').Matches.Groups[1].Value
|
||||||
|
$duration = [double]($probe | Select-String '^duration=(.+)$').Matches.Groups[1].Value
|
||||||
|
|
||||||
|
if ([int]$width -ne 1920) { throw "expected 1920 width, got $width" }
|
||||||
|
if ([int]$height -ne 1080) { throw "expected 1080 height, got $height" }
|
||||||
|
if ($fps -ne "30/1") { throw "expected 30fps, got $fps" }
|
||||||
|
if ($duration -lt 7.5 -or $duration -gt 8.5) { throw "expected ~8s duration, got $duration" }
|
||||||
|
|
||||||
|
Write-Host "canary.mp4 ok: ${width}x${height} @ $fps, ${duration}s"
|
||||||
|
|
||||||
|
- name: Upload rendered MP4 artifact
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-render-${{ github.run_id }}
|
||||||
|
path: ${{ runner.temp }}/windows-canary/canary/renders/canary.mp4
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Unit-test suites on Windows. Mirrors the Linux `test` job in ci.yml
|
||||||
|
# so we catch Windows-specific regressions (path separators, shell
|
||||||
|
# invocations, CRLF, file URLs, etc.) in existing vitest suites.
|
||||||
|
# The producer package is skipped because its tests require Docker /
|
||||||
|
# Linux-only tooling (Dockerfile.test, LFS golden MP4 baselines).
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
test-windows:
|
||||||
|
name: Tests on windows-latest
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.code == 'true' || github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.ref }}
|
||||||
|
|
||||||
|
- name: Install Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
shell: pwsh
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
shell: pwsh
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
- name: Run tests (all packages except producer)
|
||||||
|
shell: pwsh
|
||||||
|
run: bun run --filter "!@hyperframes/producer" test
|
||||||
|
|
||||||
|
- name: Run runtime contract test
|
||||||
|
shell: pwsh
|
||||||
|
run: bun run --filter "@hyperframes/core" test:hyperframe-runtime-ci
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
"@hyperframes/core": "workspace:*",
|
"@hyperframes/core": "workspace:*",
|
||||||
"@hyperframes/engine": "workspace:*",
|
"@hyperframes/engine": "workspace:*",
|
||||||
"@hyperframes/producer": "workspace:*",
|
"@hyperframes/producer": "workspace:*",
|
||||||
|
"@hyperframes/studio": "workspace:*",
|
||||||
"@types/adm-zip": "^0.5.7",
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/mime-types": "^3.0.1",
|
"@types/mime-types": "^3.0.1",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run studio",
|
"dev": "bun run studio",
|
||||||
"build": "bun run --filter '*' build",
|
"build": "bun run --filter '!@hyperframes/cli' build && bun run --filter @hyperframes/cli build",
|
||||||
"build:producer": "bun run --filter @hyperframes/producer build",
|
"build:producer": "bun run --filter @hyperframes/producer build",
|
||||||
"studio": "bun run --filter @hyperframes/studio dev",
|
"studio": "bun run --filter @hyperframes/studio dev",
|
||||||
"build:hyperframes-runtime": "bun run --filter @hyperframes/core build:hyperframes-runtime",
|
"build:hyperframes-runtime": "bun run --filter @hyperframes/core build:hyperframes-runtime",
|
||||||
|
|||||||
@@ -17,11 +17,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"dev": "tsx src/cli.ts",
|
"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:fonts": "cd ../producer && tsx scripts/generate-font-data.ts",
|
||||||
"build:studio": "cd ../studio && bun run build",
|
|
||||||
"build:runtime": "tsx scripts/build-runtime.ts",
|
"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"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -44,6 +43,7 @@
|
|||||||
"@hyperframes/core": "workspace:*",
|
"@hyperframes/core": "workspace:*",
|
||||||
"@hyperframes/engine": "workspace:*",
|
"@hyperframes/engine": "workspace:*",
|
||||||
"@hyperframes/producer": "workspace:*",
|
"@hyperframes/producer": "workspace:*",
|
||||||
|
"@hyperframes/studio": "workspace:*",
|
||||||
"@types/adm-zip": "^0.5.7",
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/mime-types": "^3.0.1",
|
"@types/mime-types": "^3.0.1",
|
||||||
"@types/node": "^22.0.0",
|
"@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.
|
* 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. */
|
/** Attributes that may contain relative asset paths. */
|
||||||
const PATH_ATTRS = ["src", "href"] as const;
|
const PATH_ATTRS = ["src", "href"] as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user