From 5421c23fffa4fb5bdb70847fe8bd713f65e69a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 1 Apr 2026 00:50:13 +0200 Subject: [PATCH] refactor(lint): break 1,314-line monolith into focused rule modules with plugin system (#170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Breaks `hyperframeLinter.ts` from 1,314 lines (one massive function) into a plugin system of focused rule modules - Introduces `LintContext` — HTML is parsed once and shared across all rules - Adds `LintRule` type as the formal contract for rules - Public API unchanged: `lintHyperframeHtml`, `lintMediaUrls`, `lintScriptUrls` signatures identical ## New file structure ``` src/lint/ utils.ts — shared types (OpenTag, ExtractedBlock), regex constants, helpers context.ts — LintContext type + buildLintContext() factory rules/ core.ts — structural rules (root attrs, timeline registry, script syntax) media.ts — media element rules (duplicate ids, video pitfalls, placeholder URLs, etc.) gsap.ts — GSAP rules + GSAP-specific parsing utils captions.ts — caption rules composition.ts — timed element, deprecated attrs, template literal selector, external scripts adapters.ts — Lottie + Three.js missing-script rules (from PR #149) hyperframeLinter.ts — orchestrator only (~200 lines, down from 1,314) ``` ## Adding a new adapter rule going forward 1. Create `src/lint/rules/my-adapter.ts` exporting `myAdapterRules: LintRule[]` 2. Import and spread into `ALL_RULES` in `hyperframeLinter.ts` ## Test plan - [x] All 402 core tests pass unchanged - [x] Full workspace build clean (`pnpm build`) - [x] TypeScript strict mode clean (`pnpm tsc --noEmit`) --- packages/core/src/lint/context.ts | 51 + .../core/src/lint/hyperframeLinter.test.ts | 147 +- packages/core/src/lint/hyperframeLinter.ts | 1180 +---------------- packages/core/src/lint/rules/adapters.ts | 53 + packages/core/src/lint/rules/captions.ts | 80 ++ packages/core/src/lint/rules/composition.ts | 109 ++ packages/core/src/lint/rules/core.ts | 169 +++ packages/core/src/lint/rules/gsap.ts | 379 ++++++ packages/core/src/lint/rules/media.ts | 225 ++++ packages/core/src/lint/types.ts | 4 + packages/core/src/lint/utils.ts | 122 ++ 11 files changed, 1384 insertions(+), 1135 deletions(-) create mode 100644 packages/core/src/lint/context.ts create mode 100644 packages/core/src/lint/rules/adapters.ts create mode 100644 packages/core/src/lint/rules/captions.ts create mode 100644 packages/core/src/lint/rules/composition.ts create mode 100644 packages/core/src/lint/rules/core.ts create mode 100644 packages/core/src/lint/rules/gsap.ts create mode 100644 packages/core/src/lint/rules/media.ts create mode 100644 packages/core/src/lint/utils.ts diff --git a/packages/core/src/lint/context.ts b/packages/core/src/lint/context.ts new file mode 100644 index 000000000..b0020ead9 --- /dev/null +++ b/packages/core/src/lint/context.ts @@ -0,0 +1,51 @@ +import type { HyperframeLintFinding, HyperframeLinterOptions } from "./types"; +import { + extractBlocks, + extractOpenTags, + findRootTag, + collectCompositionIds, + readAttr, + STYLE_BLOCK_PATTERN, + SCRIPT_BLOCK_PATTERN, +} from "./utils"; +import type { OpenTag, ExtractedBlock } from "./utils"; + +export type { OpenTag, ExtractedBlock }; + +export type LintContext = { + source: string; + tags: OpenTag[]; + styles: ExtractedBlock[]; + scripts: ExtractedBlock[]; + compositionIds: Set; + rootTag: OpenTag | null; + rootCompositionId: string | null; + options: HyperframeLinterOptions; +}; + +// Re-export for convenience so rule modules only need one import for the finding type +export type { HyperframeLintFinding }; + +export function buildLintContext(html: string, options: HyperframeLinterOptions = {}): LintContext { + let source = html || ""; + const templateMatch = source.match(/]*>([\s\S]*)<\/template>/i); + if (templateMatch?.[1]) source = templateMatch[1]; + + const tags = extractOpenTags(source); + const styles = extractBlocks(source, STYLE_BLOCK_PATTERN); + const scripts = extractBlocks(source, SCRIPT_BLOCK_PATTERN); + const compositionIds = collectCompositionIds(tags); + const rootTag = findRootTag(source); + const rootCompositionId = readAttr(rootTag?.raw || "", "data-composition-id"); + + return { + source, + tags, + styles, + scripts, + compositionIds, + rootTag, + rootCompositionId, + options, + }; +} diff --git a/packages/core/src/lint/hyperframeLinter.test.ts b/packages/core/src/lint/hyperframeLinter.test.ts index ec18fd93d..e8dbaf35d 100644 --- a/packages/core/src/lint/hyperframeLinter.test.ts +++ b/packages/core/src/lint/hyperframeLinter.test.ts @@ -115,6 +115,7 @@ describe("lintHyperframeHtml", () => { const html = `