fix(cli): resolve and install transitive registry dependencies#1396
Conversation
`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>
miguel-heygen
left a comment
There was a problem hiding this comment.
Strengths:
resolver.ts:87-96— TheresolveItemguard that throws on dep-bearing items is the right call. No future single-item caller can silently drop deps; they're forced to opt into the dep-aware API. Clean footgun elimination.resolver.ts:175-176— Cycle detection via separatevisiting/visitedsets is textbook DFS. The path argument correctly reconstructs the cycle string (traced thegamma → beta → alpha → gammacase manually — it produces the right output).add.ts:112-127—assertCompatibleOrThrowgates ALL items before any write. Fail-fast before install starts, no partial installs on compat failure.- Test coverage is solid: linear chain, single item, diamond dedup, missing dep, cycle detection, and an integration test that verifies files land on disk in the right order.
CI: All checks green (CLI smoke, Test, Typecheck, Build, windows — everything). ✅
Nit (non-blocking): resolver.ts:137-146 — getItem is typed as (): Promise<RegistryItem> but throws synchronously rather than returning Promise.reject(...). Works correctly (the async machinery in visit wraps the throw into a rejection), but the inconsistency between the return type annotation and the actual control flow is a minor maintenance hazard. Could annotate as getItem(itemName: string): Promise<RegistryItem> with a return Promise.reject(new Error(...)) to be explicit, or mark it never-returning on the throw branch. Not blocking — the behavior is correct and the tests exercise this path.
No blockers. Good rework of #414 — addresses both required-change items from that review (fetchRemoteTemplate + no out-of-scope noise) and adds the third missed path (installRegistryBlock) as a bonus.
Verdict: APPROVE
Reasoning: Correct DFS implementation with cycle detection and diamond dedup, three install paths properly threaded through the dep-aware resolver, full CI green. One nit on getItem's return-type consistency but it doesn't affect correctness.
— Magi
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>
|
Thanks @miguel-heygen — addressed the |
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>
|
Thanks @vanceingalls (Via) — these were sharp. Addressed all three in 86a9b98: 1. Compat-gate scope gap (the weighty one) — fixed. You're right that the gate only ran in 2. Integration-test scope gap — addressed, with a correction. One nuance: 3. Viewport-rewrite emergent behavior — documented + refactored. Extracted the viewport rewrite into Also fixed Magi's |
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-review on 86a9b98e — confirming each nit I raised from round 1.
✅ Fixed
getItem async nit (99b6e834) — confirmed. resolver.ts now declares const getItem = async (itemName: string): Promise<RegistryItem>, with the comment explaining the await-less body and dedup correctness preserved. Control flow and return type are now consistent.
CI: all checks green or pre-existing on main (Render on windows-latest pending but passes on main — not introduced by this PR). Approval stands.
— Magi
What
Resolve and install transitive
registryDependenciesfor registry items. AddsresolveItemWithDependencies(topological DFS) and routes all three single-item install paths through it:hyperframes add,hyperframes new(fetchRemoteTemplate), and the Studio "add block" action.This is a reworked, conflict-free, review-feedback-addressed version of #414 — which is stale (branched off an author fork's
main, carries unrelated merge commits, has been merge-conflicting againstmainfor ~7 weeks, and never addressed its two requested changes). This PR should replace and close #414.Why
registryDependencieshas been declared onRegistryItem(packages/core/src/registry/types.ts:62) and in the JSON schema for a while, with an explicit TODO in the resolver to walk it — but nothing did. Every install path resolved exactly one item and silently dropped any dependencies it declared:runAdd(hyperframes add)fetchRemoteTemplate(hyperframes new/ init template install) — flagged in Resolve and install transitive registry Dependencies for registry items #414's reviewinstallRegistryBlock(Studio "add block")No catalog item ships deps today, so this scaffolds the install path before they do (the correct order). The first dep-bearing item would otherwise produce a silently incomplete install with no error or warning.
How
resolveItemWithDependencies(name, options)inregistry/resolver.ts: DFS topological sort (dependencies first, requested item last) with cycle detection (throws with the offending path), missing-dependency errors (names the absent item), an item cache, and diamond dedup (a shared dep appears exactly once). Deps are fetched serially — a comment notes this is deliberate to keep the cycle bookkeeping simple; the registry is small.resolveItembecomes a thin guard: it throws if the item declares deps, pointing callers at the plural API. This removes the silent-discard footgun for all current and future single-item callers (directly addresses Resolve and install transitive registry Dependencies for registry items #414's review concern that the second caller —fetchRemoteTemplate— silently dropped deps).runAddresolves deps, compatibility-gates every item before any write, installs in topo order, and returns an orderedinstalled: string[]. ExtractedassertCompatibleOrThrow/installAllhelpers keep it under the complexity gate.fetchRemoteTemplateandinstallRegistryBlocknow install the full resolved list.Review feedback from #414 (James + Vai), addressed here:
fetchRemoteTemplateno longer silently discards deps (required)IMPROVEMENT_OPPORTUNITIES.md/ unrelated merge commits (required)resolved[resolved.length - 1]!)installRegistryBlock), which the original PR didn't touchI did not add a
remote.test.tsdependency test:fetchRemoteTemplatecan't passskipCache, and the manifest/item cache is on-disk keyed by URL (fetchItemManifestreads it unconditionally), so such a test is flaky on any machine that's used the real CLI. The sharedresolveItemWithDependenciesengine — whichfetchRemoteTemplatejust loops over — is fully covered by the resolver tests.Test plan
How was this tested?
resolver.test.ts: linear-chain ordering, single-item (no deps), diamond dedup, missing transitive dep, cycle detection, and theresolveItemthrow-on-deps guard.add.test.ts: transitive deps installed before the requested item with the correctinstalledorder + files on disk.bunx vitest run→ 741 passing), plustsc --noEmit,oxlint,oxfmt --check, andfallow audit --fail-on-issuesall clean.Co-authored with the original #414 author (@rakibulism), credited via
Co-authored-byon the commit — the core DFS/cycle-detection algorithm is theirs.🤖 Generated with Claude Code