mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
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:
co-authored by
Claude Opus 4.8
Rakibul Islam
parent
583b47b039
commit
8eac7e1cda
@@ -241,6 +241,39 @@ export async function loadPreviewServerBuildSignature(): Promise<string> {
|
||||
]);
|
||||
}
|
||||
|
||||
// Rewrite the viewport meta + inline width/height in every written .html to the
|
||||
// host composition's dimensions, so an installed fragment matches the host
|
||||
// canvas. Applies to ALL written files — including any .html a dependency ships,
|
||||
// not just the requested block's — which is intentional. No-op when the host
|
||||
// index.html is absent or carries no dimensions.
|
||||
function rewriteWrittenToHostViewport(projectDir: string, written: string[]): void {
|
||||
const indexPath = join(projectDir, "index.html");
|
||||
if (!existsSync(indexPath)) return;
|
||||
const indexHtml = readFileSync(indexPath, "utf-8");
|
||||
const hostW = indexHtml.match(/data-width="(\d+)"/)?.[1];
|
||||
const hostH = indexHtml.match(/data-height="(\d+)"/)?.[1];
|
||||
if (!hostW || !hostH) return;
|
||||
|
||||
for (const absPath of written) {
|
||||
if (!absPath.endsWith(".html")) continue;
|
||||
let content = readFileSync(absPath, "utf-8");
|
||||
content = content.replace(
|
||||
/(<meta\s+name="viewport"\s+content="width=)\d+(,\s*height=)\d+/i,
|
||||
`$1${hostW}$2${hostH}`,
|
||||
);
|
||||
content = content.replace(
|
||||
/(\bwidth:\s*)\d+(px;\s*\n?\s*height:\s*)\d+(px;)/g,
|
||||
(match, pre, mid, post) => {
|
||||
if (match.includes("1920") || match.includes("1080")) {
|
||||
return `${pre}${hostW}${mid}${hostH}${post}`;
|
||||
}
|
||||
return match;
|
||||
},
|
||||
);
|
||||
writeFileSync(absPath, content, "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
const { projectDir, projectName } = options;
|
||||
const projectId = projectName || basename(projectDir);
|
||||
@@ -468,39 +501,26 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
},
|
||||
|
||||
async installRegistryBlock(opts) {
|
||||
const { resolveItem } = await import("../registry/resolver.js");
|
||||
const { resolveItemWithDependencies } = await import("../registry/resolver.js");
|
||||
const { installItem } = await import("../registry/installer.js");
|
||||
const { readFileSync, writeFileSync, existsSync } = await import("node:fs");
|
||||
const { join } = await import("node:path");
|
||||
const item = await resolveItem(opts.blockName);
|
||||
const { written } = await installItem(item, { destDir: opts.project.dir });
|
||||
|
||||
const indexPath = join(opts.project.dir, "index.html");
|
||||
if (existsSync(indexPath)) {
|
||||
const indexHtml = readFileSync(indexPath, "utf-8");
|
||||
const hostW = indexHtml.match(/data-width="(\d+)"/)?.[1];
|
||||
const hostH = indexHtml.match(/data-height="(\d+)"/)?.[1];
|
||||
if (hostW && hostH) {
|
||||
for (const absPath of written) {
|
||||
if (!absPath.endsWith(".html")) continue;
|
||||
let content = readFileSync(absPath, "utf-8");
|
||||
content = content.replace(
|
||||
/(<meta\s+name="viewport"\s+content="width=)\d+(,\s*height=)\d+/i,
|
||||
`$1${hostW}$2${hostH}`,
|
||||
);
|
||||
content = content.replace(
|
||||
/(\bwidth:\s*)\d+(px;\s*\n?\s*height:\s*)\d+(px;)/g,
|
||||
(match, pre, mid, post) => {
|
||||
if (match.includes("1920") || match.includes("1080")) {
|
||||
return `${pre}${hostW}${mid}${hostH}${post}`;
|
||||
}
|
||||
return match;
|
||||
},
|
||||
);
|
||||
writeFileSync(absPath, content, "utf-8");
|
||||
}
|
||||
}
|
||||
const { gateRegistryItemsCompatibility } = await import("../registry/compatibility.js");
|
||||
// Resolve transitive registryDependencies and install them first so a
|
||||
// block that depends on other registry items installs completely.
|
||||
const items = await resolveItemWithDependencies(opts.blockName);
|
||||
// Compatibility-gate the whole set before writing anything (same gate as
|
||||
// `hyperframes add`), so an incompatible block or dep aborts cleanly.
|
||||
const warnings = gateRegistryItemsCompatibility(items);
|
||||
for (const warning of warnings) {
|
||||
process.stderr.write(`hyperframes:registry ${warning}\n`);
|
||||
}
|
||||
const written: string[] = [];
|
||||
for (const dep of items) {
|
||||
const result = await installItem(dep, { destDir: opts.project.dir });
|
||||
written.push(...result.written);
|
||||
}
|
||||
const item = items[items.length - 1]!;
|
||||
|
||||
rewriteWrittenToHostViewport(opts.project.dir, written);
|
||||
|
||||
const relativePaths = written.map((abs) => {
|
||||
const rel = abs.startsWith(opts.project.dir) ? abs.slice(opts.project.dir.length + 1) : abs;
|
||||
|
||||
Reference in New Issue
Block a user