mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
Run `fallow fix --auto-fixable` to remove `export` keywords from symbols fallow's reachability analysis identifies as unused. Keeps only the cases where the symbol is still referenced internally in its own file (so removing `export` doesn't surface a new oxlint `no-unused-vars` error). Result: fallow dead-code findings drop from 276 → 208 (68 fewer unused exports), with no behavior change — each symbol is still defined and used exactly the same way within its file. Reverted ~20 files where fallow's auto-fix would have created cascading "declared but never used" lint errors — those are cases where the symbol isn't used at all, and properly cleaning them up means deleting the declaration, not just dropping `export`. Better to land that as a separate, narrower PR rather than mixing it into a mechanical de-export. Also reverted four false positives where fallow missed real consumers: - `captureCost.ts` (renderOrchestrator has two separate import blocks from the same module; fallow only saw the first) - `propertyPanelHelpers.ts`, `domEditingLayers.ts` (real internal uses fallow's reachability missed) - `render.ts` (functions imported via `await import()` dynamic import, which fallow's static analysis doesn't follow) Test plan: bun run --filter '*' typecheck (clean), oxlint + oxfmt clean, cli/core/studio/engine vitest suites pass (335 + 917 + 576 + 605 tests).
166 lines
5.8 KiB
TypeScript
166 lines
5.8 KiB
TypeScript
// Shared types, regex constants, and utility functions used across lint rule modules.
|
|
// Nothing in this file should emit findings — it only parses and extracts.
|
|
|
|
export type OpenTag = {
|
|
raw: string;
|
|
name: string;
|
|
attrs: string;
|
|
index: number;
|
|
};
|
|
|
|
export type ExtractedBlock = {
|
|
attrs: string;
|
|
content: string;
|
|
raw: string;
|
|
index: number;
|
|
};
|
|
|
|
const TAG_PATTERN = /<([a-z][\w:-]*)(\s[^<>]*?)?>/gi;
|
|
export const STYLE_BLOCK_PATTERN = /<style\b([^>]*)>([\s\S]*?)<\/style>/gi;
|
|
export const SCRIPT_BLOCK_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
const COMPOSITION_ID_IN_CSS_PATTERN = /\[data-composition-id=["']([^"']+)["']\]/g;
|
|
export const TIMELINE_REGISTRY_INIT_PATTERN =
|
|
/window\.__timelines\s*=\s*window\.__timelines\s*\|\|\s*\{\}|window\.__timelines\s*=\s*\{\}|window\.__timelines\s*\?\?=\s*\{\}/i;
|
|
export const TIMELINE_REGISTRY_ASSIGN_PATTERN = /window\.__timelines\[[^\]]+\]\s*=/i;
|
|
export const WINDOW_TIMELINE_ASSIGN_PATTERN =
|
|
/window\.__timelines\[\s*["']([^"']+)["']\s*\]\s*=\s*([A-Za-z_$][\w$]*)/i;
|
|
export const INVALID_SCRIPT_CLOSE_PATTERN = /<script[^>]*>[\s\S]*?<\s*\/\s*script(?!>)/i;
|
|
|
|
export function extractOpenTags(source: string): OpenTag[] {
|
|
const tags: OpenTag[] = [];
|
|
let match: RegExpExecArray | null;
|
|
const pattern = new RegExp(TAG_PATTERN.source, TAG_PATTERN.flags);
|
|
while ((match = pattern.exec(source)) !== null) {
|
|
const raw = match[0];
|
|
if (raw.startsWith("</") || raw.startsWith("<!")) continue;
|
|
tags.push({
|
|
raw,
|
|
name: (match[1] || "").toLowerCase(),
|
|
attrs: match[2] || "",
|
|
index: match.index,
|
|
});
|
|
}
|
|
return tags;
|
|
}
|
|
|
|
export function extractBlocks(source: string, pattern: RegExp): ExtractedBlock[] {
|
|
const blocks: ExtractedBlock[] = [];
|
|
let match: RegExpExecArray | null;
|
|
const p = new RegExp(pattern.source, pattern.flags);
|
|
while ((match = p.exec(source)) !== null) {
|
|
blocks.push({
|
|
attrs: match[1] || "",
|
|
content: match[2] || "",
|
|
raw: match[0],
|
|
index: match.index,
|
|
});
|
|
}
|
|
return blocks;
|
|
}
|
|
|
|
/**
|
|
* Find the `<html>` open tag in the source. Distinct from `findRootTag`,
|
|
* which returns the first element inside `<body>` — the latter is "the
|
|
* composition's visible root", whereas `<html>` is where document-level
|
|
* metadata like `data-composition-variables` lives.
|
|
*/
|
|
export function findHtmlTag(source: string): OpenTag | null {
|
|
const match = /<html\b([^<>]*)>/i.exec(source);
|
|
if (!match) return null;
|
|
return {
|
|
raw: match[0],
|
|
name: "html",
|
|
attrs: match[1] ?? "",
|
|
index: match.index,
|
|
};
|
|
}
|
|
|
|
export function findRootTag(source: string): OpenTag | null {
|
|
const bodyOpenMatch = /<body\b[^>]*>/i.exec(source);
|
|
const bodyCloseMatch = /<\/body>/i.exec(source);
|
|
const bodyStart = bodyOpenMatch ? bodyOpenMatch.index + bodyOpenMatch[0].length : 0;
|
|
const bodyEnd =
|
|
bodyOpenMatch && bodyCloseMatch && bodyCloseMatch.index > bodyStart
|
|
? bodyCloseMatch.index
|
|
: source.length;
|
|
const bodyContent = bodyOpenMatch ? source.slice(bodyStart, bodyEnd) : source;
|
|
const bodyTags = extractOpenTags(bodyContent);
|
|
for (const tag of bodyTags) {
|
|
if (["script", "style", "meta", "link", "title"].includes(tag.name)) continue;
|
|
return { ...tag, index: tag.index + bodyStart };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function readAttr(tagSource: string, attr: string): string | null {
|
|
if (!tagSource) return null;
|
|
const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const match = tagSource.match(new RegExp(`\\b${escaped}\\s*=\\s*["']([^"']+)["']`, "i"));
|
|
return match?.[1] || null;
|
|
}
|
|
|
|
/**
|
|
* Read an attribute that may legitimately contain the opposite quote
|
|
* character. `readAttr` truncates `data-variable-values='{"title":"Hello"}'`
|
|
* at the first internal `"` because its `[^"']+` class excludes both quote
|
|
* types. This variant alternates: a double-quoted value never contains an
|
|
* unescaped `"`, and a single-quoted value never contains an unescaped `'`,
|
|
* so each branch can use a quote-specific class.
|
|
*
|
|
* Use for attributes whose values are JSON or otherwise carry the opposite
|
|
* quote character. Existing single-token attributes (`id`, `class`, etc.)
|
|
* stick with `readAttr` for consistency with the rest of the lint code.
|
|
*/
|
|
export function readJsonAttr(tagSource: string, attr: string): string | null {
|
|
if (!tagSource) return null;
|
|
const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const match = tagSource.match(new RegExp(`\\b${escaped}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i"));
|
|
if (!match) return null;
|
|
return match[1] ?? match[2] ?? null;
|
|
}
|
|
|
|
export function collectCompositionIds(tags: OpenTag[]): Set<string> {
|
|
const ids = new Set<string>();
|
|
for (const tag of tags) {
|
|
const compId = readAttr(tag.raw, "data-composition-id");
|
|
if (compId) ids.add(compId);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
export function extractCompositionIdsFromCss(css: string): string[] {
|
|
const ids = new Set<string>();
|
|
let match: RegExpExecArray | null;
|
|
const pattern = new RegExp(
|
|
COMPOSITION_ID_IN_CSS_PATTERN.source,
|
|
COMPOSITION_ID_IN_CSS_PATTERN.flags,
|
|
);
|
|
while ((match = pattern.exec(css)) !== null) {
|
|
if (match[1]) ids.add(match[1]);
|
|
}
|
|
return [...ids];
|
|
}
|
|
|
|
export function getInlineScriptSyntaxError(source: string): string | null {
|
|
if (!source.trim()) return null;
|
|
try {
|
|
// eslint-disable-next-line no-new-func
|
|
new Function(source);
|
|
return null;
|
|
} catch (error) {
|
|
if (error instanceof Error) return error.message;
|
|
return String(error);
|
|
}
|
|
}
|
|
|
|
export function isMediaTag(tagName: string): boolean {
|
|
return tagName === "video" || tagName === "audio" || tagName === "img";
|
|
}
|
|
|
|
export function truncateSnippet(value: string, maxLength = 220): string | undefined {
|
|
const normalized = value.replace(/\s+/g, " ").trim();
|
|
if (!normalized) return undefined;
|
|
if (normalized.length <= maxLength) return normalized;
|
|
return `${normalized.slice(0, maxLength - 3)}...`;
|
|
}
|