fix(catalog): frost and glass textures are hosted through the registry url field, not hardcoded

This commit is contained in:
Miguel Angel Simon Sierra
2026-09-18 23:44:37 -04:00
parent d120efebc1
commit 67b7ce26dc
14 changed files with 116 additions and 31 deletions
+1 -1
View File
@@ -49,7 +49,7 @@
"player:perf": "bun run --filter @hyperframes/player perf",
"format:check": "oxfmt --check .",
"knip": "knip",
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts && vitest run scripts/catalog/",
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts && vitest run scripts/catalog/",
"typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
"test:skills": "node --test 'skills/**/*.test.mjs'",
"generate:previews": "tsx scripts/generate-template-previews.ts",
@@ -22,8 +22,10 @@ Composition id: `frost-sequence-rig`. Duration 22.5 s at 30 fps, 1920×1080.
- `assets/frost.js` (1.5 MB)
- `assets/gsap-3.14.2.min.js` (71 KB)
- `assets/logo.svg` (1 KB)
- `assets/shards-atlas.png` (1.4 MB)
- `assets/test-mark.svg` (1 KB)
- `assets/textures/bluenoise64.png` (12 KB)
- `assets/textures/ice-inclusions-generated.png` (4.8 MB)
- `build.mjs` (2 KB)
- `hyperframes.json` (1 KB)
- `index.html` (119 KB)
@@ -90,7 +92,6 @@ Composition id: `frost-sequence-rig`. Duration 22.5 s at 30 fps, 1920×1080.
- `tests/shape-resolution.ts` (1 KB)
- `tests/solid-union.ts` (2 KB)
- `tools/compile-preflight.mjs` (1 KB)
- `tools/render-presets.mjs` (1 KB)
- `tools/run-shard-compile.mjs` (1 KB)
- `tools/shard-compile-probe.ts` (5 KB)
- `tools/test-shape.mjs` (1 KB)
File diff suppressed because one or more lines are too long
@@ -90,6 +90,12 @@
"target": "compositions/frost-sequence-camera-orbit/assets/logo.svg",
"type": "hyperframes:asset"
},
{
"path": "assets/shards-atlas.png",
"target": "compositions/frost-sequence-camera-orbit/assets/shards-atlas.png",
"type": "hyperframes:asset",
"url": "https://static.heygen.ai/hyperframes-oss/registry-assets/122961c82d240e47.png"
},
{
"path": "assets/test-mark.svg",
"target": "compositions/frost-sequence-camera-orbit/assets/test-mark.svg",
@@ -100,6 +106,12 @@
"target": "compositions/frost-sequence-camera-orbit/assets/textures/bluenoise64.png",
"type": "hyperframes:asset"
},
{
"path": "assets/textures/ice-inclusions-generated.png",
"target": "compositions/frost-sequence-camera-orbit/assets/textures/ice-inclusions-generated.png",
"type": "hyperframes:asset",
"url": "https://static.heygen.ai/hyperframes-oss/registry-assets/4bea3a8ab90c9527.png"
},
{
"path": "SKILL.md",
"target": "compositions/frost-sequence-camera-orbit/SKILL.md",
@@ -649,9 +649,7 @@ export function create(o: FrostOptions): FrostInstance {
return tex;
});
const fractureDetail = await new THREE.TextureLoader()
.loadAsync(
"https://static.heygen.ai/hyperframes-oss/docs/images/catalog/assets/frost-sequence-camera-orbit/textures/ice-inclusions-generated.png",
)
.loadAsync(assetUrl("textures/ice-inclusions-generated.png"))
.then((tex) => {
tex.wrapS = tex.wrapT = THREE.MirroredRepeatWrapping;
tex.minFilter = THREE.LinearMipmapLinearFilter;
@@ -1,7 +1,7 @@
// generated by scratchpad/atlas/atlas.mjs from the Higgsfield shard sheet (16 shards, 4x4)
// R,G = tangent-space normal xy, B = frost/structure, A = silhouette * (0.35 + 0.65 * thickness)
export const SHARD_ATLAS_URL =
"https://static.heygen.ai/hyperframes-oss/docs/images/catalog/assets/frost-sequence-camera-orbit/shards-atlas.png"; // FROST: was '/shards-atlas.png'
import { assetUrl } from "../assets";
export const SHARD_ATLAS_URL = assetUrl("shards-atlas.png"); // FROST: was '/shards-atlas.png'
export const SHARD_GRID = 4;
/** used extent of each cell (fraction of the cell), row-major from the top-left */
export const SHARD_CELLS: [number, number][] = [
@@ -19,6 +19,7 @@ Composition id: `glass-shard-title`. Duration 12.16 s at 30 fps, 1920×1080.
- `assets/fonts/CormorantGaramond-OFL.txt` (4 KB)
- `assets/glass-main.js` (549 KB)
- `assets/glass-main.mjs` (37 KB, ESM source `build-bundle.mjs` compiles into `glass-main.js`)
- `assets/matcap-1.png` (3.0 MB)
- `build-bundle.mjs` (1 KB)
- `compositions/glass-shard-title.html` (565 KB)
- `hyperframes.json` (1 KB)
File diff suppressed because one or more lines are too long
@@ -75,7 +75,7 @@ scene.environmentRotation = new THREE.Euler(0, 0.6, 0);
hdrTex.dispose();
pmrem.dispose();
const matcapTex = await new THREE.TextureLoader().loadAsync("https://static.heygen.ai/hyperframes-oss/docs/images/catalog/assets/glass-shard-title/matcap-1.png");
const matcapTex = await new THREE.TextureLoader().loadAsync("assets/matcap-1.png");
matcapTex.colorSpace = THREE.SRGBColorSpace;
matcapTex.wrapS = matcapTex.wrapT = THREE.RepeatWrapping;
const mk = 1 / 0.2;
@@ -61,6 +61,12 @@
"target": "compositions/glass-shard-title/assets/Three-LICENSE.txt",
"type": "hyperframes:asset"
},
{
"path": "assets/matcap-1.png",
"target": "compositions/glass-shard-title/assets/matcap-1.png",
"type": "hyperframes:asset",
"url": "https://static.heygen.ai/hyperframes-oss/registry-assets/8a4ba0deaf2bafb4.png"
},
{
"path": "SKILL.md",
"target": "compositions/glass-shard-title/SKILL.md",
+1 -12
View File
@@ -422,17 +422,6 @@ function installMirrorTargets(projectDir: string): Map<string, string> {
*/
export const MAX_HOSTED_DIRECTORY_BYTES = 2_000_000;
/**
* Named, reviewed exceptions to the cap above, each measured and one line why.
* Anything not listed here keeps the 2 MB default and falls back to its video.
*/
export const HOSTED_DIRECTORY_BYTE_ALLOWANCE: Record<string, number> = {
// Textures/fonts only, frost.js now travels in-payload. Measured 2026-09-18: 7,708,493 B.
"frost-sequence-camera-orbit": 8_500_000,
// Fonts + matcap only, glass-main.js/HDR now travel in-payload. Measured 2026-09-18: 3,387,474 B.
"glass-shard-title": 4_000_000,
};
// "not-needed" and "over-budget" both leave nothing published, but the caller
// must not treat them alike: an over-budget item still needs the directory,
// so it has to fall back to its recorded video instead of shipping with dead
@@ -501,8 +490,8 @@ export function hostItemDirectory(
projectDir: string,
destDir: string,
urlBase: string,
maxBytes: number = MAX_HOSTED_DIRECTORY_BYTES,
): HostItemDirectoryResult {
const maxBytes = MAX_HOSTED_DIRECTORY_BYTES;
const mirrorPrefix = downloadMirrorPrefix(projectDir);
const installTargets = installMirrorTargets(projectDir);
const state: ChargeState = { files: new Map(), total: 0 };
+54
View File
@@ -0,0 +1,54 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { withHostedRefs } from "./catalog-script-inlining.ts";
const CDN = "https://static.example.com/registry-assets";
function itemWith(files: { path: string; url?: string }[]): string {
const dir = mkdtempSync(join(tmpdir(), "hf-inlining-"));
const manifest = { name: "x", files: files.map((f) => ({ ...f, target: `c/${f.path}` })) };
writeFileSync(join(dir, "registry-item.json"), JSON.stringify(manifest));
return dir;
}
describe("withHostedRefs", () => {
it("points a quoted local path at its CDN URL", () => {
const dir = itemWith([{ path: "assets/matcap-1.png", url: `${CDN}/aa.png` }]);
try {
assert.equal(withHostedRefs(`load("assets/matcap-1.png")`, dir), `load("${CDN}/aa.png")`);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("replaces the bundle's base-joined helper call with the full URL", () => {
const dir = itemWith([{ path: "assets/shards-atlas.png", url: `${CDN}/bb.png` }]);
try {
assert.equal(withHostedRefs(`x=a2("shards-atlas.png")`, dir, "a2"), `x="${CDN}/bb.png"`);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("throws when a hosted name is still loaded through an unpatched helper", () => {
const dir = itemWith([{ path: "assets/shards-atlas.png", url: `${CDN}/bb.png` }]);
try {
assert.throws(() => withHostedRefs(`x=a3("shards-atlas.png")`, dir, "a2"), /still loaded/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("leaves a file that stays in the repository alone", () => {
const dir = itemWith([{ path: "assets/textures/bluenoise64.png" }]);
try {
const text = `a2("textures/bluenoise64.png")`;
assert.equal(withHostedRefs(text, dir, "a2"), text);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
+32 -2
View File
@@ -4,6 +4,8 @@
import { createHash } from "node:crypto";
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import type { RegistryItem } from "../packages/core/src/index.js";
import { hostedUrlByReference } from "./registry-hosted-assets.ts";
interface VendorFile {
/** Stable key used both as the JSON filename and the import-map value lookup. */
@@ -165,12 +167,37 @@ function extractAndRemoveScript(
};
}
/** Script text loads hosted assets by their local name, and the payload has no such file: point
* each at its CDN URL. `assetCall` is the bundle's base-joining helper, which takes the name
* relative to `assets/`. Throws when a name is still there, so a rebuild that renames things fails loudly. */
export function withHostedRefs(text: string, projectDir: string, assetCall?: string): string {
const manifest = JSON.parse(
readFileSync(join(projectDir, "registry-item.json"), "utf-8"),
) as RegistryItem;
let out = text;
for (const [ref, url] of hostedUrlByReference(manifest)) {
const names = new Set([ref, ref.replace(/^assets\//, "")]);
for (const name of names) {
if (assetCall) out = out.split(`${assetCall}("${name}")`).join(`"${url}"`);
out = out.split(`"${name}"`).join(`"${url}"`);
}
if (ref.startsWith("assets/") && out.includes(`"${ref.slice("assets/".length)}"`)) {
throw new Error(`catalog-script-inlining: "${ref}" is still loaded by its local name.`);
}
}
return out;
}
function inlineFrostScripts(
html: string,
projectDir: string,
vendorUrls: Record<string, string>,
): string {
const frostText = readFileSync(join(projectDir, "assets/frost.js"), "utf-8");
const frostText = withHostedRefs(
readFileSync(join(projectDir, "assets/frost.js"), "utf-8"),
projectDir,
"a2",
);
// Moves the composition's own inline script into the same async chain, after
// gsap/frost.js load: left in document order it would run before either is ready.
@@ -195,7 +222,10 @@ ${gsapLoaderJs(vendorUrls)}
function inlineGlassScripts(html: string, projectDir: string): string {
const hdrRelPath = "assets/ferndale_studio_01_1k.hdr";
let glassText = readFileSync(join(projectDir, "assets/glass-main.js"), "utf-8");
let glassText = withHostedRefs(
readFileSync(join(projectDir, "assets/glass-main.js"), "utf-8"),
projectDir,
);
if (!glassText.includes(hdrRelPath)) {
throw new Error(
`catalog-script-inlining: glass-main.js no longer references "${hdrRelPath}"; ` +
+1 -7
View File
@@ -44,7 +44,6 @@ import {
externalizeDataUris,
inlineMountedComposition,
HOSTED_EXTENSIONS,
HOSTED_DIRECTORY_BYTE_ALLOWANCE,
hostItemDirectory,
type HostItemDirectoryResult,
localReferences,
@@ -209,12 +208,7 @@ function resolveBaseHref(
): HostItemDirectoryResult {
if (!interactive && !needsOwnDirectory(item, unresolved)) return { status: "not-needed" };
const itemUrl = `/public/catalog/items/${item.name}`;
return hostItemDirectory(
projectDir,
join(payloadRoot, "items", item.name),
`${itemUrl}/`,
HOSTED_DIRECTORY_BYTE_ALLOWANCE[item.name],
);
return hostItemDirectory(projectDir, join(payloadRoot, "items", item.name), `${itemUrl}/`);
}
/** An interactive preview inlines the component and clears its pinned demo values,