fix(cli): resolve and install transitive registry dependencies (#1396)

* fix(cli): resolve and install transitive registry dependencies

`hyperframes add`, `hyperframes new` (fetchRemoteTemplate), and the studio
"add block" path each resolved a single registry item and silently dropped
any `registryDependencies` it declared.

Add `resolveItemWithDependencies` (DFS topological sort, cycle detection,
missing-dependency errors, and dedup of shared/diamond deps) and route all
three install paths through it so dependencies are installed before the item
that needs them. `resolveItem` becomes a thin guard that throws on dep-bearing
items, so no future caller can silently reintroduce the drop. `runAdd` now
returns the ordered `installed` list and compatibility-gates every dependency
before any write.

Reworks the stale PR #414 onto current main and addresses its review feedback:
fetchRemoteTemplate installs deps, no out-of-scope files, dead null-checks
dropped, diamond test added, and the deliberate serial-fetch tradeoff is noted.

Co-authored-by: Rakibul Islam <40rakib70@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(cli): make getItem async so missing-dep surfaces as rejection

Addresses review nit on #1396: getItem was typed Promise<RegistryItem> but
threw synchronously on a missing dependency. Marking it async keeps the
control flow consistent with the return type — the throw now becomes a
rejection. The body has no await, so the item cache is still populated
synchronously on first request and dedup is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): compatibility-gate transitive deps in all install paths

Addresses Via's review on #1396: `assertCompatibleOrThrow` only ran inside
`runAdd`, so `fetchRemoteTemplate` (hyperframes new) and the Studio
"add block" action installed resolved items — now including transitive
dependencies — with no minCliVersion enforcement or deprecation warnings. A
pre-existing single-item asymmetry that this PR's dep loops amplify across N
items.

- Add shared `gateRegistryItemsCompatibility` + `RegistryCompatibilityError`
  to compatibility.ts; all three install paths now gate the full resolved set
  before any write. `runAdd` keeps its AddError mapping by wrapping the shared
  gate.
- Surface deprecation warnings from the template/studio paths to stderr.
- Extract the studio viewport rewrite into `rewriteWrittenToHostViewport`
  (also drops redundant dynamic node:fs imports) and document that it
  intentionally rewrites dep-shipped .html too (Via item 3).
- Unit-test the shared gate directly (no fetch/cache flakiness): compatible
  set, accumulated deprecation warnings, and throw-on-incompatible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Rakibul Islam <40rakib70@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
James Russo
2026-06-12 17:18:15 -07:00
committed by GitHub
co-authored by Claude Opus 4.8 Rakibul Islam
parent 583b47b039
commit 8eac7e1cda
8 changed files with 429 additions and 74 deletions
@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";
import type { RegistryItem } from "@hyperframes/core";
import { checkRegistryItemCompatibility } from "./compatibility.js";
import {
checkRegistryItemCompatibility,
gateRegistryItemsCompatibility,
RegistryCompatibilityError,
} from "./compatibility.js";
const BASE_ITEM: RegistryItem = {
name: "demo-block",
@@ -55,3 +59,38 @@ describe("checkRegistryItemCompatibility", () => {
expect(result.error).toContain('declares invalid minCliVersion "next"');
});
});
describe("gateRegistryItemsCompatibility", () => {
const dep = (
name: string,
extra: { deprecated?: string; minCliVersion?: string } = {},
): RegistryItem => ({
...BASE_ITEM,
name,
...extra,
});
it("returns no warnings when every item is compatible", () => {
expect(gateRegistryItemsCompatibility([dep("a"), dep("b")], "0.6.79")).toEqual([]);
});
it("accumulates deprecation warnings across all items", () => {
const warnings = gateRegistryItemsCompatibility(
[dep("a", { deprecated: "gone" }), dep("b"), dep("c", { deprecated: "also gone" })],
"0.6.79",
);
expect(warnings).toEqual([
'Registry item "a" is deprecated: gone',
'Registry item "c" is deprecated: also gone',
]);
});
it("throws RegistryCompatibilityError on the first item requiring a newer CLI", () => {
expect(() =>
gateRegistryItemsCompatibility([dep("a"), dep("b", { minCliVersion: "999.0.0" })], "0.6.79"),
).toThrow(RegistryCompatibilityError);
expect(() =>
gateRegistryItemsCompatibility([dep("a"), dep("b", { minCliVersion: "999.0.0" })], "0.6.79"),
).toThrow(/requires hyperframes >= 999\.0\.0/);
});
});