mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(cli): never print "[object Object]" from validate/inspect errors (#1810)
* fix(cli): use normalizeErrorMessage so validate/inspect never print "[object Object]" The validate and inspect (layout) commands formatted thrown values with `err instanceof Error ? err.message : String(err)`. When a browser/CDP/ Puppeteer protocol error or a structured page error reaches the formatter as a plain object without a string `message`, `String(obj)` yields the useless literal "[object Object]", hiding the real cause. Route those paths through the existing shared `normalizeErrorMessage` helper, which returns an Error's message, a string as-is, an object's `.message` when present, or a compact JSON serialization otherwise (with a key-list and String fallback for circular/opaque objects). Also fold the duplicated local `errorMessage` helpers in batchRender and preview into the same shared helper. Covered by added assertions in errorMessage.test.ts for the no-message object and Puppeteer-style protocol-error object cases. * fix(cli): route remaining browser/process error sites through normalizeErrorMessage The validate/inspect fix routed only those two commands through the shared normalizeErrorMessage helper. The same err instanceof Error ? err.message : String(err) pattern survived in the other commands that drive a headless browser or an external process (ffmpeg, Docker, CDP) or surface a network API error, so a thrown structured object without a string message would still render as the useless literal [object Object]. Route those sites through the shared helper: snapshot.ts (the closest sibling to validate/inspect, same bug class), render.ts (Chrome launch + Docker build), capture/index.ts and commands/capture.ts (page-driven extraction), auth/browser.ts, browser/manager.ts (Puppeteer browser resolution), and the cloud/lambda paths (cloud/render.ts, cloudrun.ts, lambda/render-batch.ts, lambda/policies.ts, cloud/detectAspectRatio.ts) that surface API/network error objects. Only the message-deriving expression changes; control flow and error propagation are untouched. capture/index.ts keeps appending the stack for real Errors and only routes the non-Error branch. Adds a helper test for a structured CDP-style error object (code + nested data, no message).
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { c } from "../ui/colors.js";
|
||||
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
||||
|
||||
export interface OpenBrowserResult {
|
||||
/** True when we successfully invoked the platform "open" command. */
|
||||
@@ -21,7 +22,7 @@ export async function openBrowser(url: string): Promise<OpenBrowserResult> {
|
||||
await open(url);
|
||||
return { opened: true };
|
||||
} catch (err) {
|
||||
printManualInstructions(url, err instanceof Error ? err.message : String(err));
|
||||
printManualInstructions(url, normalizeErrorMessage(err));
|
||||
return { opened: false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { existsSync, readdirSync, rmSync } from "node:fs";
|
||||
import { basename } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
||||
|
||||
type PuppeteerBrowsers = typeof import("@puppeteer/browsers");
|
||||
|
||||
@@ -11,7 +12,7 @@ async function loadPuppeteerBrowsers(): Promise<PuppeteerBrowsers> {
|
||||
try {
|
||||
return await import("@puppeteer/browsers");
|
||||
} catch (err) {
|
||||
const cause = err instanceof Error ? err.message : String(err);
|
||||
const cause = normalizeErrorMessage(err);
|
||||
throw new Error(
|
||||
`Failed to load @puppeteer/browsers: ${cause}\n` +
|
||||
`Fix: run \`npm install\` or \`bun install\` to restore missing packages, then retry.`,
|
||||
@@ -268,7 +269,7 @@ export async function findBrowser(): Promise<BrowserResult | undefined> {
|
||||
try {
|
||||
return await downloadBrowser();
|
||||
} catch (err) {
|
||||
const cause = err instanceof Error ? err.message : String(err);
|
||||
const cause = normalizeErrorMessage(err);
|
||||
throw new Error(
|
||||
`Cached Chrome binary was missing at ${fromCache.staleHyperframesCachePath}, and re-download failed: ${cause}\n` +
|
||||
`Run \`hyperframes browser ensure --force\` to re-download.`,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { extractTokens } from "./tokenExtractor.js";
|
||||
import { extractDesignStyles } from "./designStyleExtractor.js";
|
||||
import { downloadAssets, downloadAndRewriteFonts } from "./assetDownloader.js";
|
||||
import { extractFontMetadata } from "./fontMetadataExtractor.js";
|
||||
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
||||
// briefGenerator.ts, visual-style, capture-summary removed — DESIGN.md replaces them
|
||||
import {
|
||||
setupAnimationCapture,
|
||||
@@ -353,7 +354,8 @@ export async function captureWebsite(
|
||||
`${designStyles.typography.length} typography roles, ${designStyles.buttons.length} button styles, ${designStyles.shadows.length} shadow values extracted`,
|
||||
);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? `${err.message}\n${err.stack}` : String(err);
|
||||
const errMsg =
|
||||
err instanceof Error ? `${err.message}\n${err.stack}` : normalizeErrorMessage(err);
|
||||
console.error(` ⚠ Design style extraction failed: ${errMsg}`);
|
||||
warnings.push(`Design style extraction failed: ${errMsg}`);
|
||||
}
|
||||
@@ -480,10 +482,7 @@ export async function captureWebsite(
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"Font metadata extraction failed (non-fatal):",
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
console.warn("Font metadata extraction failed (non-fatal):", normalizeErrorMessage(err));
|
||||
}
|
||||
|
||||
// Save animation catalog — lean version for the agent (not 745 raw CSS declarations)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
||||
|
||||
export type SupportedAspectRatio = "16:9" | "9:16" | "1:1";
|
||||
|
||||
@@ -87,7 +88,7 @@ export function detectAspectRatioFromHtml(entryHtmlPath: string): AspectRatioDet
|
||||
try {
|
||||
html = readFileSync(entryHtmlPath, "utf-8");
|
||||
} catch (err) {
|
||||
return { kind: "read-error", error: err instanceof Error ? err.message : String(err) };
|
||||
return { kind: "read-error", error: normalizeErrorMessage(err) };
|
||||
}
|
||||
return detectAspectRatioFromHtmlString(html);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, resolve, sep } from "node:path";
|
||||
import { c } from "../ui/colors.js";
|
||||
import { errorBox } from "../ui/format.js";
|
||||
import { normalizeErrorMessage as errorMessage } from "../utils/errorMessage.js";
|
||||
import {
|
||||
loadProjectVariableSchema,
|
||||
reportVariableIssues,
|
||||
@@ -90,10 +91,7 @@ function parseJson(raw: string, source: string): unknown {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error: unknown) {
|
||||
throw new BatchRenderInputError(
|
||||
"Invalid JSON in --batch",
|
||||
`${source}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
throw new BatchRenderInputError("Invalid JSON in --batch", `${source}: ${errorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +237,7 @@ export function prepareBatchRender(options: PrepareBatchRenderOptions): Prepared
|
||||
} catch (error: unknown) {
|
||||
throw new BatchRenderInputError(
|
||||
"Could not read --batch",
|
||||
`${batchPath}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
`${batchPath}: ${errorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -310,10 +308,6 @@ function emitJsonEvent(event: Record<string, unknown>, json: boolean): void {
|
||||
if (json) console.log(JSON.stringify(event));
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
async function renderBatchRow(
|
||||
row: PreparedBatchRow,
|
||||
manifest: BatchManifest,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineCommand } from "citty";
|
||||
import { resolve } from "node:path";
|
||||
import type { Example } from "./_examples.js";
|
||||
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
||||
|
||||
export const examples: Example[] = [
|
||||
["Capture a website into ./capture/", "hyperframes capture https://stripe.com"],
|
||||
@@ -213,7 +214,7 @@ export default defineCommand({
|
||||
console.log();
|
||||
}
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
const errMsg = normalizeErrorMessage(err);
|
||||
// Write BLOCKED.md so the user/agent knows the capture failed
|
||||
try {
|
||||
const { mkdirSync, writeFileSync } = await import("node:fs");
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import { c } from "../../ui/colors.js";
|
||||
import { errorBox, formatBytes, formatDuration } from "../../ui/format.js";
|
||||
import { resolveProject } from "../../utils/project.js";
|
||||
import { normalizeErrorMessage } from "../../utils/errorMessage.js";
|
||||
import { createPublishArchive } from "../../utils/publishProject.js";
|
||||
import {
|
||||
reportVariableIssues,
|
||||
@@ -541,7 +542,7 @@ async function maybeUploadProject(
|
||||
try {
|
||||
archive = createPublishArchive(project.dir);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const msg = normalizeErrorMessage(err);
|
||||
errorBox("Zip failed", msg, "Check the project for missing files or unreadable permissions.");
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -733,7 +734,7 @@ async function streamVideo(
|
||||
}
|
||||
return { bytes: result.bytes };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = normalizeErrorMessage(err);
|
||||
errorBox(
|
||||
"Download failed",
|
||||
message,
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
resolveVariablesArg,
|
||||
validateVariablesAgainstProject,
|
||||
} from "../utils/variables.js";
|
||||
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
||||
|
||||
export const examples: Example[] = [
|
||||
["Deploy the Cloud Run render stack", "hyperframes cloudrun deploy --project my-gcp-project"],
|
||||
@@ -670,7 +671,7 @@ async function runRenderBatch(args: Record<string, unknown>): Promise<void> {
|
||||
} catch (err) {
|
||||
return {
|
||||
outputKey: entry.outputKey,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: normalizeErrorMessage(err),
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { c } from "../../ui/colors.js";
|
||||
import { normalizeErrorMessage } from "../../utils/errorMessage.js";
|
||||
|
||||
export type PoliciesVerb = "role" | "user" | "validate";
|
||||
|
||||
@@ -247,7 +248,7 @@ export async function runPolicies(args: PoliciesArgs): Promise<void> {
|
||||
try {
|
||||
result = validatePolicy(args.inputPath);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const msg = normalizeErrorMessage(err);
|
||||
if (args.json) {
|
||||
console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
|
||||
process.exitCode = 1;
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
reportVariableIssues,
|
||||
validateVariablesAgainstSchema,
|
||||
} from "../../utils/variables.js";
|
||||
import { normalizeErrorMessage } from "../../utils/errorMessage.js";
|
||||
import { warnOnDimensionMismatch } from "./_dimensions.js";
|
||||
import { requireStack } from "./state.js";
|
||||
|
||||
@@ -294,7 +295,7 @@ export async function runRenderBatch(args: RenderBatchArgs): Promise<void> {
|
||||
outputKey: entry.outputKey,
|
||||
executionArn: null,
|
||||
status: "failed-to-start",
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: normalizeErrorMessage(err),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -371,10 +372,7 @@ export function parseBatchFile(path: string): Array<{ entry: BatchEntry; lineNum
|
||||
try {
|
||||
parsed = JSON.parse(line);
|
||||
} catch (err) {
|
||||
errorBox(
|
||||
`Invalid JSON in batch file on line ${i + 1}`,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
);
|
||||
errorBox(`Invalid JSON in batch file on line ${i + 1}`, normalizeErrorMessage(err));
|
||||
process.exit(1);
|
||||
}
|
||||
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
|
||||
import type { Example } from "./_examples.js";
|
||||
import { c } from "../ui/colors.js";
|
||||
import { resolveProject } from "../utils/project.js";
|
||||
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
||||
import { serveStaticProjectHtml } from "../utils/staticProjectServer.js";
|
||||
import { withMeta } from "../utils/updateCheck.js";
|
||||
import {
|
||||
@@ -634,7 +635,7 @@ export function createInspectCommand(commandName: "inspect" | "layout") {
|
||||
|
||||
process.exit(ok ? 0 : 1);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = normalizeErrorMessage(err);
|
||||
if (args.json) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
|
||||
@@ -26,6 +26,7 @@ import { createRequire } from "node:module";
|
||||
import * as clack from "@clack/prompts";
|
||||
import { c } from "../ui/colors.js";
|
||||
import { isDevMode } from "../utils/env.js";
|
||||
import { normalizeErrorMessage as errorMessage } from "../utils/errorMessage.js";
|
||||
import { buildNpxCommand } from "../utils/npxCommand.js";
|
||||
import type { StudioSelectionSnapshot } from "@hyperframes/studio-server";
|
||||
import {
|
||||
@@ -307,10 +308,6 @@ function printSelectionFailure(code: string, message: string, json: boolean): vo
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function previewServerPayload(server: {
|
||||
port: number;
|
||||
host?: string;
|
||||
@@ -471,11 +468,7 @@ async function printCurrentContext(
|
||||
try {
|
||||
fields = parseContextFields(options.fields);
|
||||
} catch (err) {
|
||||
printSelectionFailure(
|
||||
"invalid-context-fields",
|
||||
err instanceof Error ? err.message : String(err),
|
||||
options.json,
|
||||
);
|
||||
printSelectionFailure("invalid-context-fields", errorMessage(err), options.json);
|
||||
return;
|
||||
}
|
||||
const fullDetail = options.detail === "full";
|
||||
|
||||
@@ -753,7 +753,7 @@ export default defineCommand({
|
||||
browserSpinner?.stop(c.error("Browser not available"));
|
||||
errorBox(
|
||||
"Chrome not found",
|
||||
err instanceof Error ? err.message : String(err),
|
||||
normalizeErrorMessage(err),
|
||||
"Run: npx hyperframes browser ensure",
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -1076,7 +1076,7 @@ function ensureDockerImage(version: string, platform: string, quiet: boolean): s
|
||||
{ stdio: quiet ? "pipe" : "inherit", timeout: 600_000 },
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const message = normalizeErrorMessage(error);
|
||||
throw new Error(`Failed to build Docker image: ${message}`);
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
@@ -1149,7 +1149,7 @@ async function renderDocker(
|
||||
try {
|
||||
imageTag = ensureDockerImage(dockerVersion, platform, options.quiet);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const message = normalizeErrorMessage(error);
|
||||
const isDockerMissing = /connect|not found|ENOENT/i.test(message);
|
||||
errorBox(
|
||||
isDockerMissing ? "Docker not available" : "Docker image build failed",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { existsSync, mkdtempSync, readFileSync, mkdirSync, rmSync, writeFileSync
|
||||
import { tmpdir } from "node:os";
|
||||
import { resolve, join, relative, isAbsolute, basename } from "node:path";
|
||||
import { resolveProject } from "../utils/project.js";
|
||||
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
||||
import { resolveCompositionViewportFromHtml } from "../utils/compositionViewport.js";
|
||||
import { serveStaticProjectHtml } from "../utils/staticProjectServer.js";
|
||||
import { c } from "../ui/colors.js";
|
||||
@@ -594,8 +595,7 @@ export default defineCommand({
|
||||
descriptions.push(`## ${result.value.filename}`, `${result.value.desc}`, ``);
|
||||
} else {
|
||||
// Log first failure so Gemini issues are visible rather than silent
|
||||
const errMsg =
|
||||
result.reason instanceof Error ? result.reason.message : String(result.reason);
|
||||
const errMsg = normalizeErrorMessage(result.reason);
|
||||
descriptions.push(`## (error)`, `Gemini call failed: ${errMsg.slice(0, 120)}`, ``);
|
||||
}
|
||||
}
|
||||
@@ -605,12 +605,12 @@ export default defineCommand({
|
||||
console.log(` ${c.dim("descriptions.md")} (Gemini frame analysis)`);
|
||||
}
|
||||
} catch (descErr) {
|
||||
const msg = descErr instanceof Error ? descErr.message : String(descErr);
|
||||
const msg = normalizeErrorMessage(descErr);
|
||||
console.log(` ${c.dim(`--describe failed: ${msg.slice(0, 80)}`)}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const msg = normalizeErrorMessage(err);
|
||||
console.error(`\n${c.error("✗")} Snapshot failed: ${msg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveProject } from "../utils/project.js";
|
||||
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
||||
import { resolveCompositionViewportFromHtml } from "../utils/compositionViewport.js";
|
||||
import { c } from "../ui/colors.js";
|
||||
import { withMeta } from "../utils/updateCheck.js";
|
||||
@@ -226,7 +227,7 @@ async function validateInBrowser(
|
||||
});
|
||||
|
||||
page.on("pageerror", (err) => {
|
||||
const text = err instanceof Error ? err.message : String(err);
|
||||
const text = normalizeErrorMessage(err);
|
||||
// CDN scripts (e.g. GSAP from jsdelivr) returning HTML error pages
|
||||
// instead of JS produce "Unexpected token '<'" SyntaxErrors. These
|
||||
// are network failures, not composition authoring errors.
|
||||
@@ -394,7 +395,7 @@ Examples:
|
||||
const exitCode = printValidationResult(result, asJson);
|
||||
process.exit(exitCode);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = normalizeErrorMessage(err);
|
||||
emitFailureReport(message, asJson);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,26 @@ describe("normalizeErrorMessage", () => {
|
||||
expect(normalizeErrorMessage(hostile)).toBe("[object Object]");
|
||||
});
|
||||
|
||||
it("never yields '[object Object]' for a no-message object (the reported validate/inspect bug)", () => {
|
||||
const out = normalizeErrorMessage({ code: 42 });
|
||||
expect(out).not.toBe("[object Object]");
|
||||
expect(out).toContain("42");
|
||||
});
|
||||
|
||||
it("surfaces a Puppeteer-style protocol error object via its message", () => {
|
||||
expect(normalizeErrorMessage({ name: "ProtocolError", message: "Target closed" })).toBe(
|
||||
"Target closed",
|
||||
);
|
||||
});
|
||||
|
||||
it("surfaces a structured CDP error object (no message) instead of '[object Object]'", () => {
|
||||
// The shape snapshot/render can receive when a CDP/protocol rejection is a
|
||||
// plain object rather than an Error: no string `message`, only code + data.
|
||||
const out = normalizeErrorMessage({ code: -32000, data: { reason: "navigation timeout" } });
|
||||
expect(out).not.toBe("[object Object]");
|
||||
expect(out).toContain("navigation timeout");
|
||||
});
|
||||
|
||||
it("returns 'unknown error' for null and undefined", () => {
|
||||
expect(normalizeErrorMessage(null)).toBe("unknown error");
|
||||
expect(normalizeErrorMessage(undefined)).toBe("unknown error");
|
||||
|
||||
Reference in New Issue
Block a user