mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix: make registry blocks pass lint errors (#564)
This commit is contained in:
@@ -181,9 +181,9 @@ describe("runAdd (integration, mocked registry)", () => {
|
||||
expect(result.type).toBe("hyperframes:block");
|
||||
expect(result.written).toHaveLength(1);
|
||||
expect(existsSync(join(dir, "compositions/my-block.html"))).toBe(true);
|
||||
expect(readFileSync(join(dir, "compositions/my-block.html"), "utf-8")).toContain(
|
||||
"my-block.html",
|
||||
);
|
||||
const installed = readFileSync(join(dir, "compositions/my-block.html"), "utf-8");
|
||||
expect(installed).toContain("<!-- hyperframes-registry-item: my-block -->");
|
||||
expect(installed).toContain("my-block.html");
|
||||
expect(result.snippet).toContain("compositions/my-block.html");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* runtime to reject traversal even if the registry JSON schema was bypassed.
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve, relative, isAbsolute } from "node:path";
|
||||
import type { FileTarget, RegistryItem } from "@hyperframes/core";
|
||||
import { fetchItemFile, DEFAULT_REGISTRY_URL } from "./remote.js";
|
||||
@@ -45,6 +46,22 @@ export function assertSafeTarget(destDir: string, target: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function isInstalledRegistryBlockComposition(item: RegistryItem, file: FileTarget): boolean {
|
||||
return (
|
||||
item.type === "hyperframes:block" &&
|
||||
file.type === "hyperframes:composition" &&
|
||||
file.target.toLowerCase().endsWith(".html")
|
||||
);
|
||||
}
|
||||
|
||||
function addRegistryItemMarker(source: string, item: RegistryItem): string {
|
||||
if (/^\s*<!--\s*hyperframes-registry-item:[^>]*-->/i.test(source.slice(0, 512))) {
|
||||
return source;
|
||||
}
|
||||
|
||||
return `<!-- hyperframes-registry-item: ${item.name} -->\n${source}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a resolved `RegistryItem` into `destDir` by fetching each file in
|
||||
* parallel and writing it to its validated target path.
|
||||
@@ -65,6 +82,10 @@ export async function installItem(
|
||||
item.files.map(async (file: FileTarget) => {
|
||||
const destPath = resolve(destDir, file.target);
|
||||
await fetchItemFile(item, file, destPath, baseUrl);
|
||||
if (isInstalledRegistryBlockComposition(item, file)) {
|
||||
const source = readFileSync(destPath, "utf-8");
|
||||
writeFileSync(destPath, addRegistryItemMarker(source, item), "utf-8");
|
||||
}
|
||||
return destPath;
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -35,19 +35,45 @@ describe("composition rules", () => {
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("warns on large HTML files regardless of path", () => {
|
||||
it("does not warn for large registry source block files", () => {
|
||||
const html = Array.from({ length: 301 }, (_, i) =>
|
||||
i === 0 ? "<html><body>" : `<!-- filler ${i} -->`,
|
||||
).join("\n");
|
||||
|
||||
const result = lintHyperframeHtml(html, {
|
||||
filePath: "/project/registry/blocks/data-chart.html",
|
||||
filePath: "/project/registry/blocks/data-chart/data-chart.html",
|
||||
});
|
||||
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("warns for large installed block composition files", () => {
|
||||
const html = Array.from({ length: 301 }, (_, i) =>
|
||||
i === 0 ? "<html><body>" : `<!-- filler ${i} -->`,
|
||||
).join("\n");
|
||||
|
||||
const result = lintHyperframeHtml(html, {
|
||||
filePath: "/project/compositions/data-chart.html",
|
||||
});
|
||||
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
|
||||
expect(finding).toBeDefined();
|
||||
expect(finding?.severity).toBe("warning");
|
||||
});
|
||||
|
||||
it("does not warn for large registry-installed block composition files", () => {
|
||||
const html =
|
||||
"<!-- hyperframes-registry-item: data-chart -->\n" +
|
||||
Array.from({ length: 300 }, (_, i) =>
|
||||
i === 0 ? "<html><body>" : `<!-- filler ${i} -->`,
|
||||
).join("\n");
|
||||
|
||||
const result = lintHyperframeHtml(html, {
|
||||
filePath: "/project/compositions/data-chart.html",
|
||||
});
|
||||
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses nested split copy for large sub-composition files", () => {
|
||||
const html = Array.from({ length: 301 }, (_, i) =>
|
||||
i === 0 ? "<html><body>" : `<!-- filler ${i} -->`,
|
||||
|
||||
@@ -15,6 +15,17 @@ function countPhysicalLines(source: string): number {
|
||||
return withoutFinalNewline.split("\n").length;
|
||||
}
|
||||
|
||||
function isRegistrySourceFile(filePath?: string): boolean {
|
||||
if (!filePath) return false;
|
||||
|
||||
const normalized = filePath.replace(/\\/g, "/");
|
||||
return /(?:^|\/)registry\/blocks\/([^/]+)\/\1\.html$/i.test(normalized);
|
||||
}
|
||||
|
||||
function isRegistryInstalledFile(rawSource: string): boolean {
|
||||
return /^\s*<!--\s*hyperframes-registry-item:[^>]*-->/i.test(rawSource.slice(0, 512));
|
||||
}
|
||||
|
||||
function isCompositionRootOrMount(rawTag: string): boolean {
|
||||
return Boolean(
|
||||
readAttr(rawTag, "data-composition-id") || readAttr(rawTag, "data-composition-src"),
|
||||
@@ -24,6 +35,8 @@ function isCompositionRootOrMount(rawTag: string): boolean {
|
||||
export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
// composition_file_too_large
|
||||
({ rawSource, options }) => {
|
||||
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
|
||||
|
||||
const lineCount = countPhysicalLines(rawSource);
|
||||
if (lineCount <= MAX_COMPOSITION_LINES) return [];
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
data-composition-id="app-showcase"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="5.5"
|
||||
>
|
||||
<!-- Background -->
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
+1
@@ -33,6 +33,7 @@
|
||||
data-composition-id="data-chart"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="15"
|
||||
>
|
||||
<div class="chart-container">
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
+31
-23
@@ -33,6 +33,7 @@
|
||||
data-composition-id="flowchart"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
>
|
||||
<div class="canvas">
|
||||
@@ -309,32 +310,32 @@
|
||||
(function () {
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const S = '[data-composition-id="flowchart"]';
|
||||
const nodePython = document.querySelector(`${S} #node-python`);
|
||||
const pythonText = document.querySelector(`${S} #python-text`);
|
||||
const nodePython = document.querySelector(S + " #node-python");
|
||||
const pythonText = document.querySelector(S + " #python-text");
|
||||
|
||||
// 1. Root node scales in
|
||||
tl.to(`${S} #node-root`, { scale: 1, duration: 0.4, ease: "power2.out" });
|
||||
tl.to(S + " #node-root", { scale: 1, duration: 0.4, ease: "power2.out" });
|
||||
|
||||
// 2. Hold
|
||||
tl.addLabel("hold1", "+=0.6");
|
||||
|
||||
// 3. Connectors draw
|
||||
tl.to(
|
||||
`${S} .connector#path-1-L, ${S} .connector#path-1-R`,
|
||||
S + " .connector#path-1-L, " + S + " .connector#path-1-R",
|
||||
{ strokeDashoffset: 0, duration: 0.5, ease: "none" },
|
||||
"hold1",
|
||||
);
|
||||
|
||||
tl.to(
|
||||
`${S} #label-yes, ${S} #label-not-sure`,
|
||||
S + " #label-yes, " + S + " #label-not-sure",
|
||||
{ opacity: 1, duration: 0.2 },
|
||||
"hold1+=0.25",
|
||||
);
|
||||
|
||||
// 4. Level 2 nodes
|
||||
tl.to(`${S} #node-yes`, { scale: 1, duration: 0.4, ease: "back.out(1.7)" }, "hold1+=0.4");
|
||||
tl.to(S + " #node-yes", { scale: 1, duration: 0.4, ease: "back.out(1.7)" }, "hold1+=0.4");
|
||||
tl.to(
|
||||
`${S} #node-not-sure`,
|
||||
S + " #node-not-sure",
|
||||
{ scale: 1, duration: 0.4, ease: "back.out(1.7)" },
|
||||
"hold1+=0.6",
|
||||
);
|
||||
@@ -344,17 +345,24 @@
|
||||
|
||||
// 6. Level 2→3 connectors
|
||||
tl.to(
|
||||
`${S} .connector#path-2-LL, ${S} .connector#path-2-LR, ${S} .connector#path-2-RL, ${S} .connector#path-2-RR`,
|
||||
S +
|
||||
" .connector#path-2-LL, " +
|
||||
S +
|
||||
" .connector#path-2-LR, " +
|
||||
S +
|
||||
" .connector#path-2-RL, " +
|
||||
S +
|
||||
" .connector#path-2-RR",
|
||||
{ strokeDashoffset: 0, duration: 0.4, ease: "none" },
|
||||
"hold2",
|
||||
);
|
||||
|
||||
// 7. Leaf nodes
|
||||
const leafNodes = [
|
||||
`${S} #node-python`,
|
||||
`${S} #node-nocode`,
|
||||
`${S} #node-website`,
|
||||
`${S} #node-course`,
|
||||
S + " #node-python",
|
||||
S + " #node-nocode",
|
||||
S + " #node-website",
|
||||
S + " #node-course",
|
||||
];
|
||||
leafNodes.forEach((id, i) => {
|
||||
tl.to(
|
||||
@@ -364,14 +372,14 @@
|
||||
);
|
||||
});
|
||||
|
||||
tl.to(`${S} .squiggle-container`, { opacity: 1, duration: 0.1 }, "hold2+=0.3");
|
||||
tl.to(S + " .squiggle-container", { opacity: 1, duration: 0.1 }, "hold2+=0.3");
|
||||
|
||||
// 8. Hold
|
||||
tl.addLabel("hold3", "+=0.8");
|
||||
|
||||
// 9. Cursor drifts in
|
||||
tl.to(
|
||||
`${S} #cursor`,
|
||||
S + " #cursor",
|
||||
{ left: 450, top: 540, duration: 1, ease: "power1.inOut" },
|
||||
"hold3",
|
||||
);
|
||||
@@ -384,11 +392,11 @@
|
||||
border.className = "selection-border";
|
||||
nodePython.appendChild(border);
|
||||
}
|
||||
gsap.set(`${S} .selection-border`, { opacity: 1 });
|
||||
gsap.set(S + " .selection-border", { opacity: 1 });
|
||||
}, "hold3+=1");
|
||||
|
||||
tl.to(
|
||||
`${S} #cursor`,
|
||||
S + " #cursor",
|
||||
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" },
|
||||
"hold3+=1",
|
||||
);
|
||||
@@ -398,7 +406,7 @@
|
||||
|
||||
// 12. Double-click to highlight "Pythom"
|
||||
tl.to(
|
||||
`${S} #cursor`,
|
||||
S + " #cursor",
|
||||
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 3, overwrite: "auto" },
|
||||
"hold4",
|
||||
);
|
||||
@@ -426,24 +434,24 @@
|
||||
const typingEnd = typingStart + words.length * 0.05;
|
||||
tl.add(() => {
|
||||
if (pythonText) pythonText.innerHTML = "Start with Python";
|
||||
gsap.set(`${S} .squiggle-container`, { opacity: 0 });
|
||||
gsap.set(S + " .squiggle-container", { opacity: 0 });
|
||||
}, typingEnd);
|
||||
|
||||
// 15. Hold
|
||||
tl.addLabel("hold6", typingEnd + 0.5);
|
||||
|
||||
// 16. Cursor clicks away to deselect
|
||||
tl.to(`${S} #cursor`, { left: 500, top: 580, duration: 0.3, overwrite: "auto" }, "hold6");
|
||||
tl.to(S + " #cursor", { left: 500, top: 580, duration: 0.3, overwrite: "auto" }, "hold6");
|
||||
tl.to(
|
||||
`${S} #cursor`,
|
||||
S + " #cursor",
|
||||
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" },
|
||||
"hold6+=0.3",
|
||||
);
|
||||
tl.to(`${S} .selection-border`, { opacity: 0, duration: 0.1 }, "hold6+=0.3");
|
||||
tl.to(S + " .selection-border", { opacity: 0, duration: 0.1 }, "hold6+=0.3");
|
||||
|
||||
// 17. Emoji pop
|
||||
tl.to(
|
||||
`${S} #emoji-thumb`,
|
||||
S + " #emoji-thumb",
|
||||
{ scale: 1, duration: 0.15, ease: "back.out(2)" },
|
||||
"hold6+=0.4",
|
||||
);
|
||||
@@ -452,7 +460,7 @@
|
||||
tl.addLabel("hold7", "+=2");
|
||||
|
||||
// 19. Fade out
|
||||
tl.to(`${S} #fade-overlay`, { opacity: 1, duration: 0.5 }, "hold7");
|
||||
tl.to(S + " #fade-overlay", { opacity: 1, duration: 0.5 }, "hold7");
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["flowchart"] = tl;
|
||||
|
||||
Vendored
+1
@@ -190,6 +190,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
@@ -189,6 +189,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
+1
@@ -189,6 +189,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
+1
@@ -33,6 +33,7 @@
|
||||
data-composition-id="logo-outro"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
>
|
||||
<div class="canvas">
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
+1
@@ -190,6 +190,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
@@ -189,6 +189,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
+8
-1
@@ -28,7 +28,13 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div data-composition-id="ui-3d-reveal" data-width="1920" data-height="1080" data-duration="13">
|
||||
<div
|
||||
data-composition-id="ui-3d-reveal"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="13"
|
||||
>
|
||||
<!-- Background: Figma outer canvas dark -->
|
||||
<div class="bg-base"></div>
|
||||
<div class="bg-glow" id="bgGlow"></div>
|
||||
@@ -2897,6 +2903,7 @@
|
||||
O + 4.8,
|
||||
);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["ui-3d-reveal"] = tl;
|
||||
})();
|
||||
</script>
|
||||
|
||||
+1
@@ -190,6 +190,7 @@
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
|
||||
Reference in New Issue
Block a user