From 18e1a76f1195c828af41390a3fc4a903dc7ce94f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 30 Mar 2026 20:24:01 +0200 Subject: [PATCH] feat(lint): add external_script_dependency rule for CDN scripts in sub-compositions Added info-level lint rule that flags compositions loading external CDN libraries via + +
+
+
+ +`, + "compositions/rockets.html": ``, + }); + + const bundled = await bundleToSingleHtml(dir); + + // Lottie CDN script from sub-composition must be present in the bundle + expect(bundled).toContain( + "https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/lottie.min.js", + ); + + // Should only appear once (deduped) + const occurrences = (bundled.match(/cdnjs\.cloudflare\.com\/ajax\/libs\/lottie-web/g) ?? []) + .length; + expect(occurrences).toBe(1); + + // GSAP CDN from main doc should still be present + expect(bundled).toContain("cdn.jsdelivr.net/npm/gsap"); + + // data-composition-src should be stripped (composition was inlined) + expect(bundled).not.toContain("data-composition-src"); + }); + + it("does not duplicate CDN scripts already present in the main document", async () => { + const dir = makeTempProject({ + "index.html": ` + + + +
+
+
+ +`, + "compositions/child.html": ``, + }); + + const bundled = await bundleToSingleHtml(dir); + + // GSAP CDN should appear exactly once (deduped) + const gsapOccurrences = ( + bundled.match(/cdn\.jsdelivr\.net\/npm\/gsap@3\.14\.2\/dist\/gsap\.min\.js/g) ?? [] + ).length; + expect(gsapOccurrences).toBe(1); + }); +}); diff --git a/packages/core/src/lint/hyperframeLinter.test.ts b/packages/core/src/lint/hyperframeLinter.test.ts index 1a5d31758..a5ef30ce4 100644 --- a/packages/core/src/lint/hyperframeLinter.test.ts +++ b/packages/core/src/lint/hyperframeLinter.test.ts @@ -111,6 +111,42 @@ describe("lintHyperframeHtml", () => { expect(codes.length).toBe(uniqueCodes.length); }); + it("reports info for composition with external CDN script dependency", () => { + const html = ``; + const result = lintHyperframeHtml(html, { filePath: "compositions/rockets.html" }); + const finding = result.findings.find((f) => f.code === "external_script_dependency"); + expect(finding).toBeDefined(); + expect(finding?.severity).toBe("info"); + expect(finding?.message).toContain("cdnjs.cloudflare.com"); + // info findings do not count as errors — ok should still be true + expect(result.ok).toBe(true); + expect(result.errorCount).toBe(0); + }); + + it("does not report external_script_dependency for inline scripts", () => { + const html = ` + +
+ +
+`; + const result = lintHyperframeHtml(html); + expect(result.findings.find((f) => f.code === "external_script_dependency")).toBeUndefined(); + }); + it("strips