feat(cli): deprecate validate, inspect, layout in favor of check

One stderr notice per invocation and _meta.deprecated: true in JSON mode
(shared helper next to withMeta; layout owns both inspect and layout via
createInspectCommand). Help descriptions gain the pointer. No behavior
change; removal ships separately once migration telemetry says usage
has decayed.

fix(producer): route info/debug logs to stderr — the compiler's
'Localized remote media' line was landing on stdout ahead of validate's
--json payload, breaking every piped consumer. Diagnostics now share
stderr with warn/error; render progress uses its own channel.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-10 13:27:52 -04:00
parent 3a02942a03
commit 58f45ef758
9 changed files with 500 additions and 86 deletions
+85 -1
View File
@@ -1,4 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import type { CommandDef } from "citty";
import { runCommand } from "citty";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
extractCompositionErrorsFromLint,
navigationTimeoutHint,
@@ -28,6 +30,29 @@ vi.mock("../utils/producer.js", () => ({
})),
}));
// U5 deprecation tests: resolveProject and lintProject are both reached via a
// dynamic `await import(...)` inside validate.ts's run() / validateInBrowser(),
// so vi.mock intercepts them the same way it would a static import. Mocking
// resolveProject skips real filesystem project resolution; mocking lintProject
// (the first await inside validateInBrowser) gives a fast, deterministic
// failure well before any real browser or network work — exercising run()'s
// outer catch (the JSON failure envelope) without needing headless Chrome.
const FAKE_PROJECT = {
dir: "/fake-project",
name: "fake-project",
indexPath: "/fake-project/index.html",
};
vi.mock("../utils/project.js", () => ({
resolveProject: vi.fn(() => FAKE_PROJECT),
}));
vi.mock("../utils/lintProject.js", () => ({
lintProject: vi.fn(async () => {
throw new Error("lint failed (test double)");
}),
}));
// Regression for the validate audio-duration-probe timeout: a slow-loading
// media element's duration was snapshotted once, at a fixed point in time,
// and any element still mid-load was permanently misreported as unreadable.
@@ -282,3 +307,62 @@ describe("navigationTimeoutHint", () => {
expect(navigationTimeoutHint("some string failure", 10000)).toBeNull();
});
});
function metaDescription(command: CommandDef): string {
const meta = command.meta;
if (meta && typeof meta === "object" && "description" in meta) {
return String(meta.description ?? "");
}
throw new Error("expected a synchronous meta object");
}
describe("validate command deprecation (U5)", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("marks the command description as deprecated", async () => {
const { default: validateCommand } = await import("./validate.js");
expect(metaDescription(validateCommand)).toContain("(deprecated, use check)");
});
it("prints a one-line deprecation notice to stderr and never to stdout", async () => {
const stderrWrites: string[] = [];
const stdoutWrites: string[] = [];
vi.spyOn(process.stderr, "write").mockImplementation((chunk: unknown) => {
stderrWrites.push(String(chunk));
return true;
});
vi.spyOn(process.stdout, "write").mockImplementation((chunk: unknown) => {
stdoutWrites.push(String(chunk));
return true;
});
vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
vi.spyOn(console, "log").mockImplementation(() => {});
const { default: validateCommand } = await import("./validate.js");
await runCommand(validateCommand, { rawArgs: ["--json"] });
const stderrText = stderrWrites.join("");
expect(stderrText).toContain("hyperframes validate");
expect(stderrText).toContain("hyperframes check");
expect(stdoutWrites.join("")).toBe("");
});
it("--json output is valid JSON with _meta.deprecated === true on failure", async () => {
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const { default: validateCommand } = await import("./validate.js");
await runCommand(validateCommand, { rawArgs: ["--json"] });
const jsonCall = logSpy.mock.calls.find(
([arg]) => typeof arg === "string" && arg.trim().startsWith("{"),
);
expect(jsonCall).toBeDefined();
const parsed = JSON.parse(String(jsonCall?.[0]));
expect(parsed.ok).toBe(false);
expect(parsed._meta.deprecated).toBe(true);
});
});