diff --git a/packages/cli/src/commands/add.test.ts b/packages/cli/src/commands/add.test.ts index 87b82f525..6836b20d0 100644 --- a/packages/cli/src/commands/add.test.ts +++ b/packages/cli/src/commands/add.test.ts @@ -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(""); + expect(installed).toContain("my-block.html"); expect(result.snippet).toContain("compositions/my-block.html"); } finally { rmSync(dir, { recursive: true, force: true }); diff --git a/packages/cli/src/registry/installer.ts b/packages/cli/src/registry/installer.ts index 5ba6d2cde..4ce4314e1 100644 --- a/packages/cli/src/registry/installer.ts +++ b/packages/cli/src/registry/installer.ts @@ -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*/i.test(source.slice(0, 512))) { + return source; + } + + return `\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; }), ); diff --git a/packages/core/src/lint/rules/composition.test.ts b/packages/core/src/lint/rules/composition.test.ts index 484eb9273..72fad8465 100644 --- a/packages/core/src/lint/rules/composition.test.ts +++ b/packages/core/src/lint/rules/composition.test.ts @@ -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 ? "
" : ``, ).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 ? "" : ``, + ).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 = + "\n" + + Array.from({ length: 300 }, (_, i) => + i === 0 ? "" : ``, + ).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 ? "" : ``, diff --git a/packages/core/src/lint/rules/composition.ts b/packages/core/src/lint/rules/composition.ts index 65e738898..27d6f078a 100644 --- a/packages/core/src/lint/rules/composition.ts +++ b/packages/core/src/lint/rules/composition.ts @@ -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*/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 []; diff --git a/registry/blocks/app-showcase/app-showcase.html b/registry/blocks/app-showcase/app-showcase.html index deb3887d4..52b7c2d10 100644 --- a/registry/blocks/app-showcase/app-showcase.html +++ b/registry/blocks/app-showcase/app-showcase.html @@ -25,6 +25,7 @@ data-composition-id="app-showcase" data-width="1920" data-height="1080" + data-start="0" data-duration="5.5" > diff --git a/registry/blocks/chromatic-radial-split/chromatic-radial-split.html b/registry/blocks/chromatic-radial-split/chromatic-radial-split.html index 1785f5b83..cdbd1c5d6 100644 --- a/registry/blocks/chromatic-radial-split/chromatic-radial-split.html +++ b/registry/blocks/chromatic-radial-split/chromatic-radial-split.html @@ -190,6 +190,7 @@