Skip to content

Commit 0e73278

Browse files
committed
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.
1 parent 36c7dff commit 0e73278

3 files changed

Lines changed: 87 additions & 47 deletions

File tree

packages/cli/src/commands/render.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -901,12 +901,15 @@ export async function renderLocal(
901901
await producer.executeRenderJob(job, projectDir, outputPath, onProgress);
902902
} catch (error: unknown) {
903903
maybeConsumeDeParallelRouterTrial(deParallelRouterActive, job, options.quiet);
904+
// The render container sets `ENV CONTAINER=true`; suggesting `--docker`
905+
// from inside it is a misdirection (heygen-com/hyperframes#3370).
906+
const inContainer = process.env.CONTAINER === "true";
904907
handleRenderError(
905908
error,
906909
options,
907910
startTime,
908911
false,
909-
"Try --docker for containerized rendering",
912+
inContainer ? "" : "Try --docker for containerized rendering",
910913
job.failedStage,
911914
job,
912915
);

packages/producer/src/services/hyperframeRuntimeLoader.test.ts

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,40 +36,71 @@ describe("resolveHyperframeManifestPath", () => {
3636
expect(SIBLING_PATH).toContain("producer/src/services/hyperframe.manifest.json");
3737
});
3838

39-
it("includes sibling path as first candidate in resolution order", async () => {
40-
// Import the actual source and verify the sibling path is found when it
41-
// exists. In the monorepo, the monorepo-relative path also exists, so we
42-
// verify the sibling would win by checking its position in candidates.
43-
//
44-
// We can't easily mock existsSync in ESM, but we CAN verify the
45-
// structural invariant: the function checks SIBLING first by reading the
46-
// source and confirming the candidate array order.
47-
const { readFileSync } = await import("node:fs");
48-
const source = readFileSync(resolve(THIS_DIR, "hyperframeRuntimeLoader.ts"), "utf8");
49-
50-
// The candidates array must list SIBLING_MANIFEST_PATH before the others
51-
const candidatesMatch = source.match(/const candidates = \[([\s\S]*?)\];/);
52-
expect(candidatesMatch).not.toBeNull();
53-
const candidatesBody = candidatesMatch![1];
54-
55-
const siblingIdx = candidatesBody.indexOf("SIBLING_MANIFEST_PATH");
56-
const cwdIdx = candidatesBody.indexOf("CWD_RELATIVE_MANIFEST_PATHS");
57-
const moduleIdx = candidatesBody.indexOf("MODULE_RELATIVE_MANIFEST_PATH");
58-
59-
expect(siblingIdx).toBeGreaterThan(-1);
60-
expect(siblingIdx).toBeLessThan(cwdIdx);
61-
expect(cwdIdx).toBeLessThan(moduleIdx);
39+
it("prefers sibling path when it exists, otherwise picks the first existing candidate", async () => {
40+
// Behaviour-level replacement for the old source-text test that
41+
// asserted on string positions inside `const candidates = [...]`. We
42+
// prove the behavioural invariant instead: the resolver returns the
43+
// first candidate that actually exists on disk.
44+
const { resolveHyperframeManifestPath } = await import("./hyperframeRuntimeLoader.js");
45+
const resolved = resolveHyperframeManifestPath();
46+
expect(existsSync(resolved)).toBe(true);
47+
// The sibling would win when present. In dev, the monorepo-relative
48+
// core/dist is the real fallback; either way the path must exist.
49+
if (existsSync(SIBLING_PATH)) {
50+
expect(resolved).toBe(SIBLING_PATH);
51+
}
6252
});
6353

64-
it("finds manifest via monorepo-relative path in dev (integration check)", async () => {
65-
// In the monorepo, the core/dist manifest should exist from the build.
66-
// This acts as a smoke test that the resolution works in the dev env.
54+
it("falls back to MONOREPO_PATH when present in dev (smoke test)", async () => {
6755
if (!existsSync(MONOREPO_PATH)) {
6856
// Skip if core hasn't been built — this is expected in CI before build
6957
return;
7058
}
7159
const { resolveHyperframeManifestPath } = await import("./hyperframeRuntimeLoader.js");
72-
const result = resolveHyperframeManifestPath();
73-
expect(existsSync(result)).toBe(true);
60+
expect(resolveHyperframeManifestPath()).toBe(MONOREPO_PATH);
61+
});
62+
});
63+
64+
describe("hyperframeRuntimeLoader error path (#3370)", () => {
65+
const originalEnv = process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH;
66+
67+
beforeEach(() => {
68+
delete process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH;
69+
});
70+
71+
afterEach(() => {
72+
if (originalEnv !== undefined) {
73+
process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH = originalEnv;
74+
} else {
75+
delete process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH;
76+
}
77+
});
78+
79+
it("names the env-override path when PRODUCER_HYPERFRAME_MANIFEST_PATH is set and missing", async () => {
80+
// Force the env-override branch with a missing file. The thrown error
81+
// must name the override, not any fallback candidate.
82+
process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH = "/nonexistent/override/manifest.json";
83+
const { resolveVerifiedHyperframeRuntime } = await import("./hyperframeRuntimeLoader.js");
84+
expect(() => resolveVerifiedHyperframeRuntime()).toThrow(
85+
/nonexistent\/override\/manifest\.json/,
86+
);
87+
});
88+
89+
it("triedManifestPaths returns only the override when PRODUCER_HYPERFRAME_MANIFEST_PATH is set", async () => {
90+
process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH = "/another/missing/override.json";
91+
const { triedManifestPaths } = await import("./hyperframeRuntimeLoader.js");
92+
expect(triedManifestPaths()).toEqual(["/another/missing/override.json"]);
93+
});
94+
95+
it("triedManifestPaths lists every candidate when no override is set", async () => {
96+
delete process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH;
97+
const { triedManifestPaths } = await import("./hyperframeRuntimeLoader.js");
98+
const tried = triedManifestPaths();
99+
expect(tried.length).toBeGreaterThanOrEqual(4);
100+
// The first candidate must be the sibling path so the user sees it
101+
// first in the error message (heygen-com/hyperframes#3370).
102+
expect(tried[0]).toBe(
103+
resolve(dirname(fileURLToPath(import.meta.url)), "hyperframe.manifest.json"),
104+
);
74105
});
75106
});

packages/producer/src/services/hyperframeRuntimeLoader.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ const MODULE_RELATIVE_MANIFEST_PATH = resolve(
99
PRODUCER_DIR,
1010
"../../../core/dist/hyperframe.manifest.json",
1111
);
12-
const CWD_RELATIVE_MANIFEST_PATHS = [
13-
// When bundled to a single file (dist/public-server.js), the manifest
14-
// is copied as a sibling by build.mjs
15-
resolve(PRODUCER_DIR, "hyperframe.manifest.json"),
12+
// Order matters: a bundled CLI ships the manifest as a sibling of the
13+
// packaged module; dev runs reach it via monorepo-relative paths. Listed
14+
// once here so the resolver and the missing-manifest error share the same
15+
// owner — printing only the fallback candidate misdirects the user
16+
// (heygen-com/hyperframes#3370).
17+
const MANIFEST_CANDIDATES: readonly string[] = [
18+
SIBLING_MANIFEST_PATH,
1619
resolve(process.cwd(), "packages/core/dist/hyperframe.manifest.json"),
1720
resolve(process.cwd(), "../core/dist/hyperframe.manifest.json"),
1821
resolve(process.cwd(), "core/dist/hyperframe.manifest.json"),
22+
MODULE_RELATIVE_MANIFEST_PATH,
1923
];
2024

2125
type HyperframeRuntimeManifest = {
@@ -34,20 +38,21 @@ export type ResolvedHyperframeRuntime = {
3438
};
3539

3640
export function resolveHyperframeManifestPath(): string {
37-
if (process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH) {
38-
return process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH;
41+
const envOverride = process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH;
42+
if (envOverride) {
43+
return envOverride;
3944
}
40-
const candidates = [
41-
SIBLING_MANIFEST_PATH,
42-
...CWD_RELATIVE_MANIFEST_PATHS,
43-
MODULE_RELATIVE_MANIFEST_PATH,
44-
];
45-
for (const candidate of candidates) {
46-
if (existsSync(candidate)) {
47-
return candidate;
48-
}
49-
}
50-
return MODULE_RELATIVE_MANIFEST_PATH;
45+
const found = MANIFEST_CANDIDATES.find((candidate) => existsSync(candidate));
46+
// Fall back to the last candidate only when nothing exists. The caller will
47+
// read its iife/artifact and throw; returning a stable but unreachable path
48+
// keeps the existing API contract.
49+
return found ?? MODULE_RELATIVE_MANIFEST_PATH;
50+
}
51+
52+
export function triedManifestPaths(): readonly string[] {
53+
return process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH
54+
? [process.env.PRODUCER_HYPERFRAME_MANIFEST_PATH]
55+
: MANIFEST_CANDIDATES;
5156
}
5257

5358
export function getVerifiedHyperframeRuntimeSource(): string {
@@ -57,8 +62,9 @@ export function getVerifiedHyperframeRuntimeSource(): string {
5762
export function resolveVerifiedHyperframeRuntime(): ResolvedHyperframeRuntime {
5863
const manifestPath = resolveHyperframeManifestPath();
5964
if (!existsSync(manifestPath)) {
65+
const tried = triedManifestPaths().join(", ");
6066
throw new Error(
61-
`[HyperframeRuntimeLoader] Missing manifest at ${manifestPath}. Build core runtime artifacts before rendering.`,
67+
`[HyperframeRuntimeLoader] Missing manifest. Tried: ${tried}. Searched from cwd=${process.cwd()}. Build core runtime artifacts before rendering.`,
6268
);
6369
}
6470

0 commit comments

Comments
 (0)