From 0e73278694b221393d2b8b8ceecc3d64ca7689bb Mon Sep 17 00:00:00 2001 From: Santhi Prakash Date: Fri, 21 Aug 2026 05:38:15 +0000 Subject: [PATCH] fix(producer,cli): surface every tried manifest path in the missing-manifest error (#3370) When the hyperframe runtime manifest could not be located, the loader reported a single fallback path that was never searched for (`/usr/local/lib/core/dist/hyperframe.manifest.json`), so users looked in the wrong directory. * Hoist the candidate list to a single `MANIFEST_CANDIDATES` owner shared by the resolver and the error reporter. The missing-manifest message now lists every path actually checked plus the cwd. * Drop the byte-identical duplicate of `SIBLING_MANIFEST_PATH` inside what was mislabelled `CWD_RELATIVE_MANIFEST_PATHS`. * Replace the source-text regex test with a behaviour test that points `PRODUCER_HYPERFRAME_MANIFEST_PATH` at a missing file and asserts the thrown error names it. * Suppress the "Try --docker" hint inside the render container (Dockerfile.render sets `ENV CONTAINER=true`), so users already in the container don't get told to run the same fallback they're in. --- packages/cli/src/commands/render.ts | 5 +- .../services/hyperframeRuntimeLoader.test.ts | 87 +++++++++++++------ .../src/services/hyperframeRuntimeLoader.ts | 42 +++++---- 3 files changed, 87 insertions(+), 47 deletions(-) diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index 06d03da67d..c1f538536b 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -901,12 +901,15 @@ export async function renderLocal( await producer.executeRenderJob(job, projectDir, outputPath, onProgress); } catch (error: unknown) { maybeConsumeDeParallelRouterTrial(deParallelRouterActive, job, options.quiet); + // The render container sets `ENV CONTAINER=true`; suggesting `--docker` + // from inside it is a misdirection (heygen-com/hyperframes#3370). + const inContainer = process.env.CONTAINER === "true"; handleRenderError( error, options, startTime, false, - "Try --docker for containerized rendering", + inContainer ? "" : "Try --docker for containerized rendering", job.failedStage, job, ); diff --git a/packages/producer/src/services/hyperframeRuntimeLoader.test.ts b/packages/producer/src/services/hyperframeRuntimeLoader.test.ts index 73530ccd05..a34654ef7a 100644 --- a/packages/producer/src/services/hyperframeRuntimeLoader.test.ts +++ b/packages/producer/src/services/hyperframeRuntimeLoader.test.ts @@ -36,40 +36,71 @@ describe("resolveHyperframeManifestPath", () => { expect(SIBLING_PATH).toContain("producer/src/services/hyperframe.manifest.json"); }); - it("includes sibling path as first candidate in resolution order", async () => { - // Import the actual source and verify the sibling path is found when it - // exists. In the monorepo, the monorepo-relative path also exists, so we - // verify the sibling would win by checking its position in candidates. - // - // We can't easily mock existsSync in ESM, but we CAN verify the - // structural invariant: the function checks SIBLING first by reading the - // source and confirming the candidate array order. - const { readFileSync } = await import("node:fs"); - const source = readFileSync(resolve(THIS_DIR, "hyperframeRuntimeLoader.ts"), "utf8"); - - // The candidates array must list SIBLING_MANIFEST_PATH before the others - const candidatesMatch = source.match(/const candidates = \[([\s\S]*?)\];/); - expect(candidatesMatch).not.toBeNull(); - const candidatesBody = candidatesMatch![1]; - - const siblingIdx = candidatesBody.indexOf("SIBLING_MANIFEST_PATH"); - const cwdIdx = candidatesBody.indexOf("CWD_RELATIVE_MANIFEST_PATHS"); - const moduleIdx = candidatesBody.indexOf("MODULE_RELATIVE_MANIFEST_PATH"); - - expect(siblingIdx).toBeGreaterThan(-1); - expect(siblingIdx).toBeLessThan(cwdIdx); - expect(cwdIdx).toBeLessThan(moduleIdx); + it("prefers sibling path when it exists, otherwise picks the first existing candidate", async () => { + // Behaviour-level replacement for the old source-text test that + // asserted on string positions inside `const candidates = [...]`. We + // prove the behavioural invariant instead: the resolver returns the + // first candidate that actually exists on disk. + const { resolveHyperframeManifestPath } = await import("./hyperframeRuntimeLoader.js"); + const resolved = resolveHyperframeManifestPath(); + expect(existsSync(resolved)).toBe(true); + // The sibling would win when present. In dev, the monorepo-relative + // core/dist is the real fallback; either way the path must exist. + if (existsSync(SIBLING_PATH)) { + expect(resolved).toBe(SIBLING_PATH); + } }); - it("finds manifest via monorepo-relative path in dev (integration check)", async () => { - // In the monorepo, the core/dist manifest should exist from the build. - // This acts as a smoke test that the resolution works in the dev env. + it("falls back to MONOREPO_PATH when present in dev (smoke test)", async () => { if (!existsSync(MONOREPO_PATH)) { // Skip if core hasn't been built — this is expected in CI before build return; } const { resolveHyperframeManifestPath } = await import("./hyperframeRuntimeLoader.js"); - const result = resolveHyperframeManifestPath(); - expect(existsSync(result)).toBe(true); + expect(resolveHyperframeManifestPath()).toBe(MONOREPO_PATH); + }); +}); + +describe("hyperframeRuntimeLoader error path (#3370)", () => { + const originalEnv = process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH; + + beforeEach(() => { + delete process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH; + }); + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH = originalEnv; + } else { + delete process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH; + } + }); + + it("names the env-override path when PRODUCER_HYPERFRAME_MANIFEST_PATH is set and missing", async () => { + // Force the env-override branch with a missing file. The thrown error + // must name the override, not any fallback candidate. + process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH = "/nonexistent/override/manifest.json"; + const { resolveVerifiedHyperframeRuntime } = await import("./hyperframeRuntimeLoader.js"); + expect(() => resolveVerifiedHyperframeRuntime()).toThrow( + /nonexistent\/override\/manifest\.json/, + ); + }); + + it("triedManifestPaths returns only the override when PRODUCER_HYPERFRAME_MANIFEST_PATH is set", async () => { + process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH = "/another/missing/override.json"; + const { triedManifestPaths } = await import("./hyperframeRuntimeLoader.js"); + expect(triedManifestPaths()).toEqual(["/another/missing/override.json"]); + }); + + it("triedManifestPaths lists every candidate when no override is set", async () => { + delete process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH; + const { triedManifestPaths } = await import("./hyperframeRuntimeLoader.js"); + const tried = triedManifestPaths(); + expect(tried.length).toBeGreaterThanOrEqual(4); + // The first candidate must be the sibling path so the user sees it + // first in the error message (heygen-com/hyperframes#3370). + expect(tried[0]).toBe( + resolve(dirname(fileURLToPath(import.meta.url)), "hyperframe.manifest.json"), + ); }); }); diff --git a/packages/producer/src/services/hyperframeRuntimeLoader.ts b/packages/producer/src/services/hyperframeRuntimeLoader.ts index 64e72f6a6e..c45c8e1205 100644 --- a/packages/producer/src/services/hyperframeRuntimeLoader.ts +++ b/packages/producer/src/services/hyperframeRuntimeLoader.ts @@ -9,13 +9,17 @@ 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"), +// Order matters: a bundled CLI ships the manifest as a sibling of the +// packaged module; dev runs reach it via monorepo-relative paths. Listed +// once here so the resolver and the missing-manifest error share the same +// owner — printing only the fallback candidate misdirects the user +// (heygen-com/hyperframes#3370). +const MANIFEST_CANDIDATES: readonly string[] = [ + SIBLING_MANIFEST_PATH, 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"), + MODULE_RELATIVE_MANIFEST_PATH, ]; type HyperframeRuntimeManifest = { @@ -34,20 +38,21 @@ export type ResolvedHyperframeRuntime = { }; export function resolveHyperframeManifestPath(): string { - if (process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH) { - return process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH; + const envOverride = process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH; + if (envOverride) { + return envOverride; } - 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; + const found = MANIFEST_CANDIDATES.find((candidate) => existsSync(candidate)); + // Fall back to the last candidate only when nothing exists. The caller will + // read its iife/artifact and throw; returning a stable but unreachable path + // keeps the existing API contract. + return found ?? MODULE_RELATIVE_MANIFEST_PATH; +} + +export function triedManifestPaths(): readonly string[] { + return process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH + ? [process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH] + : MANIFEST_CANDIDATES; } export function getVerifiedHyperframeRuntimeSource(): string { @@ -57,8 +62,9 @@ export function getVerifiedHyperframeRuntimeSource(): string { export function resolveVerifiedHyperframeRuntime(): ResolvedHyperframeRuntime { const manifestPath = resolveHyperframeManifestPath(); if (!existsSync(manifestPath)) { + const tried = triedManifestPaths().join(", "); throw new Error( - `[HyperframeRuntimeLoader] Missing manifest at ${manifestPath}. Build core runtime artifacts before rendering.`, + `[HyperframeRuntimeLoader] Missing manifest. Tried: ${tried}. Searched from cwd=${process.cwd()}. Build core runtime artifacts before rendering.`, ); }