Files
hyperframes/packages/producer/src/services/hyperframeRuntimeLoader.ts
T
Miguel Ángel dc0d69a21b feat(producer): support entryFile for rendering individual compositions (#42)
## Changes

- Add optional `entryFile` parameter to render API endpoints (`/v1/render` and `/v1/render-stream`)
- Enable rendering individual sub-compositions by extracting them from index.html context when the entry file is a `<template>` wrapper
- Change base64 audio/video linting from detecting "fabricated" media to prohibiting all inline base64 media
- Add manifest path resolution for bundled producer deployments

## API Changes

- `RenderConfig` — new optional `entryFile` field for specifying HTML file to render
- `server.ts` — parses `entryFile` from request body, validates file exists in project directory
- `executeRenderJob` — uses `entryFile` instead of hardcoded `"index.html"`

## Template Extraction

- `extractStandaloneEntryFromIndex` — extracts sub-composition hosts from index.html and creates standalone render context
- Handles `<template>` entry files by finding matching `data-composition-src` in index.html and isolating that host
- Resets `data-start` to 0 for standalone rendering

## Linting Updates

- Change rule #3.7 from detecting "fabricated" base64 media to prohibiting all inline base64 audio/video
- Lower detection threshold from 100+ to 20+ base64 characters
- All base64 media now triggers error severity with clearer messaging about file size bloat

## Usage

```json
POST /v1/render-stream
{ "projectDir": "/path/to/project", "entryFile": "compositions/intro.html" }
```

Omit `entryFile` for default behavior (renders `index.html`).
2026-03-27 04:28:51 +01:00

94 lines
3.0 KiB
TypeScript

import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const PRODUCER_DIR = dirname(fileURLToPath(import.meta.url));
const SIBLING_MANIFEST_PATH = resolve(PRODUCER_DIR, "hyperframe.manifest.json");
const MODULE_RELATIVE_MANIFEST_PATH = resolve(
PRODUCER_DIR,
"../../../core/dist/hyperframe.manifest.json",
);
const CWD_RELATIVE_MANIFEST_PATHS = [
// When bundled to a single file (dist/public-server.js), the manifest
// is copied as a sibling by build.mjs
resolve(PRODUCER_DIR, "hyperframe.manifest.json"),
resolve(process.cwd(), "packages/core/dist/hyperframe.manifest.json"),
resolve(process.cwd(), "../core/dist/hyperframe.manifest.json"),
resolve(process.cwd(), "core/dist/hyperframe.manifest.json"),
];
type HyperframeRuntimeManifest = {
sha256?: string;
artifacts?: {
iife?: string;
};
};
export type ResolvedHyperframeRuntime = {
manifestPath: string;
runtimePath: string;
expectedSha256: string;
actualSha256: string;
runtimeSource: string;
};
export function resolveHyperframeManifestPath(): string {
if (process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH) {
return process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH;
}
const candidates = [
SIBLING_MANIFEST_PATH,
...CWD_RELATIVE_MANIFEST_PATHS,
MODULE_RELATIVE_MANIFEST_PATH,
];
for (const candidate of candidates) {
if (existsSync(candidate)) {
return candidate;
}
}
return MODULE_RELATIVE_MANIFEST_PATH;
}
export function getVerifiedHyperframeRuntimeSource(): string {
return resolveVerifiedHyperframeRuntime().runtimeSource;
}
export function resolveVerifiedHyperframeRuntime(): ResolvedHyperframeRuntime {
const manifestPath = resolveHyperframeManifestPath();
if (!existsSync(manifestPath)) {
throw new Error(
`[HyperframeRuntimeLoader] Missing manifest at ${manifestPath}. Build core runtime artifacts before rendering.`,
);
}
const manifestRaw = readFileSync(manifestPath, "utf8");
const manifest = JSON.parse(manifestRaw) as HyperframeRuntimeManifest;
const runtimeFileName = manifest.artifacts?.iife;
if (!runtimeFileName || !manifest.sha256) {
throw new Error(
`[HyperframeRuntimeLoader] Invalid manifest at ${manifestPath}; missing iife artifact or sha256.`,
);
}
const runtimePath = resolve(dirname(manifestPath), runtimeFileName);
if (!existsSync(runtimePath)) {
throw new Error(`[HyperframeRuntimeLoader] Missing runtime artifact at ${runtimePath}.`);
}
const runtimeSource = readFileSync(runtimePath, "utf8");
const runtimeSha = createHash("sha256").update(runtimeSource, "utf8").digest("hex");
if (runtimeSha !== manifest.sha256) {
throw new Error(
`[HyperframeRuntimeLoader] Runtime checksum mismatch. expected=${manifest.sha256} actual=${runtimeSha}`,
);
}
return {
manifestPath,
runtimePath,
expectedSha256: manifest.sha256,
actualSha256: runtimeSha,
runtimeSource,
};
}