Files
hyperframes/packages/core/src/lint/context.ts
T
Miguel Ángel 5421c23fff refactor(lint): break 1,314-line monolith into focused rule modules with plugin system (#170)
## 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<TContext>` 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`)
2026-04-01 00:50:13 +02:00

52 lines
1.4 KiB
TypeScript

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<string>;
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(/<template[^>]*>([\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,
};
}