mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(cli): validate project directory before starting preview (#1394)
Preview previously started Studio even when the path was invalid (e.g. `hyperframes preview #`), yielding an empty project view. Align preview with lint/render by resolving the project up front, and add a clearer error when `#` is passed as a directory argument. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
||||
type FindPortResult,
|
||||
} from "../server/portUtils.js";
|
||||
import { killOrphanedProcesses, killProcessTree } from "../utils/orphanCleanup.js";
|
||||
import { resolveProject } from "../utils/project.js";
|
||||
|
||||
export default defineCommand({
|
||||
meta: { name: "preview", description: "Start the studio for previewing compositions" },
|
||||
@@ -118,23 +119,18 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
const rawArg = args.dir;
|
||||
const dir = resolve(rawArg ?? ".");
|
||||
|
||||
// Compute display name: preserve symlink/CWD name when user runs "hyperframes preview ."
|
||||
const isImplicitCwd = !rawArg || rawArg === "." || rawArg === "./";
|
||||
const projectName = isImplicitCwd ? basename(process.env.PWD ?? dir) : basename(dir);
|
||||
const project = resolveProject(rawArg);
|
||||
const dir = project.dir;
|
||||
const indexPath = project.indexPath;
|
||||
const projectName = isImplicitCwd ? basename(process.env.PWD ?? dir) : project.name;
|
||||
|
||||
// Lint before starting — surface issues for the agent to fix.
|
||||
// preview.ts doesn't use resolveProject() because it needs to proceed even without index.html.
|
||||
const indexPath = join(dir, "index.html");
|
||||
if (existsSync(indexPath)) {
|
||||
const project = { dir, name: projectName, indexPath };
|
||||
const lintResult = await lintProject(project);
|
||||
if (lintResult.totalErrors > 0 || lintResult.totalWarnings > 0) {
|
||||
console.log();
|
||||
for (const line of formatLintFindings(lintResult)) console.log(line);
|
||||
console.log();
|
||||
}
|
||||
const lintResult = await lintProject({ dir, name: projectName, indexPath });
|
||||
if (lintResult.totalErrors > 0 || lintResult.totalWarnings > 0) {
|
||||
console.log();
|
||||
for (const line of formatLintFindings(lintResult)) console.log(line);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Validation: --user-data-dir requires --browser-path
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, basename } from "node:path";
|
||||
import { InvalidProjectError, resolveProjectOrThrow } from "./project.js";
|
||||
|
||||
describe("resolveProjectOrThrow", () => {
|
||||
it("rejects # as a project directory with a helpful message", () => {
|
||||
try {
|
||||
resolveProjectOrThrow("#");
|
||||
expect.unreachable("expected InvalidProjectError");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(InvalidProjectError);
|
||||
const error = err as InvalidProjectError;
|
||||
expect(error.title).toBe("Invalid project directory: #");
|
||||
expect(error.hint).toContain("URL fragment");
|
||||
expect(error.suggestion).toContain("hyperframes preview .");
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a missing directory", () => {
|
||||
const missing = join(tmpdir(), `hf-missing-${Date.now()}`);
|
||||
expect(() => resolveProjectOrThrow(missing)).toThrowError(/Not a directory/);
|
||||
});
|
||||
|
||||
it("rejects a directory without index.html", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-empty-project-"));
|
||||
try {
|
||||
expect(() => resolveProjectOrThrow(dir)).toThrowError(/No composition found/);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts a directory with index.html", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-valid-project-"));
|
||||
try {
|
||||
writeFileSync(join(dir, "index.html"), '<html data-composition-id="test"></html>');
|
||||
const project = resolveProjectOrThrow(dir);
|
||||
expect(project.dir).toBe(dir);
|
||||
expect(project.indexPath).toBe(join(dir, "index.html"));
|
||||
expect(project.name).toBe(basename(dir));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -8,23 +8,56 @@ export interface ProjectDir {
|
||||
indexPath: string;
|
||||
}
|
||||
|
||||
export function resolveProject(dirArg: string | undefined): ProjectDir {
|
||||
export class InvalidProjectError extends Error {
|
||||
readonly title: string;
|
||||
readonly hint?: string;
|
||||
readonly suggestion?: string;
|
||||
|
||||
constructor(title: string, hint?: string, suggestion?: string) {
|
||||
super(title);
|
||||
this.name = "InvalidProjectError";
|
||||
this.title = title;
|
||||
this.hint = hint;
|
||||
this.suggestion = suggestion;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveProjectOrThrow(dirArg: string | undefined): ProjectDir {
|
||||
const trimmed = dirArg?.trim();
|
||||
if (trimmed === "#") {
|
||||
throw new InvalidProjectError(
|
||||
"Invalid project directory: #",
|
||||
"# is a URL fragment, not a project path.",
|
||||
"Run hyperframes preview . from your project directory.",
|
||||
);
|
||||
}
|
||||
|
||||
const dir = resolve(dirArg ?? ".");
|
||||
const name = basename(dir);
|
||||
const indexPath = resolve(dir, "index.html");
|
||||
|
||||
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
||||
errorBox("Not a directory: " + dir);
|
||||
process.exit(1);
|
||||
throw new InvalidProjectError("Not a directory: " + dir);
|
||||
}
|
||||
if (!existsSync(indexPath)) {
|
||||
errorBox(
|
||||
throw new InvalidProjectError(
|
||||
"No composition found in " + dir,
|
||||
"No index.html file found.",
|
||||
"Run npx hyperframes init to create a new composition.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return { dir, name, indexPath };
|
||||
}
|
||||
|
||||
export function resolveProject(dirArg: string | undefined): ProjectDir {
|
||||
try {
|
||||
return resolveProjectOrThrow(dirArg);
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidProjectError) {
|
||||
errorBox(err.title, err.hint, err.suggestion);
|
||||
process.exit(1);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user