mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
feat(lint): add rules for missing data-start, template wrapper, and DOCTYPE
Three new lint rules that catch structural issues causing compositions to fail silently in preview: - root_composition_missing_data_start: Root composition needs data-start="0" for the runtime to begin playback - standalone_composition_wrapped_in_template: index.html should not be wrapped in <template> (only sub-compositions use that) - root_composition_missing_html_wrapper: index.html needs <!DOCTYPE html> and <html> wrapper for the bundler Also adds rawSource to LintContext so rules can inspect pre-template-stripped HTML, and isSubComposition to linter options so rules can distinguish root from sub-composition files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b33bbfa0f9
commit
fc973ee2e8
@@ -40,7 +40,7 @@ export function lintProject(project: ProjectDir): ProjectLintResult {
|
||||
const filePath = join(compositionsDir, file);
|
||||
const html = readFileSync(filePath, "utf-8");
|
||||
allHtmlSources.push(html);
|
||||
const result = lintHyperframeHtml(html, { filePath });
|
||||
const result = lintHyperframeHtml(html, { filePath, isSubComposition: true });
|
||||
results.push({ file: `compositions/${file}`, result });
|
||||
totalErrors += result.errorCount;
|
||||
totalWarnings += result.warningCount;
|
||||
|
||||
@@ -14,6 +14,7 @@ export type { OpenTag, ExtractedBlock };
|
||||
|
||||
export type LintContext = {
|
||||
source: string;
|
||||
rawSource: string;
|
||||
tags: OpenTag[];
|
||||
styles: ExtractedBlock[];
|
||||
scripts: ExtractedBlock[];
|
||||
@@ -27,7 +28,8 @@ export type LintContext = {
|
||||
export type { HyperframeLintFinding };
|
||||
|
||||
export function buildLintContext(html: string, options: HyperframeLinterOptions = {}): LintContext {
|
||||
let source = html || "";
|
||||
const rawSource = html || "";
|
||||
let source = rawSource;
|
||||
const templateMatch = source.match(/<template[^>]*>([\s\S]*)<\/template>/i);
|
||||
if (templateMatch?.[1]) source = templateMatch[1];
|
||||
|
||||
@@ -40,6 +42,7 @@ export function buildLintContext(html: string, options: HyperframeLinterOptions
|
||||
|
||||
return {
|
||||
source,
|
||||
rawSource,
|
||||
tags,
|
||||
styles,
|
||||
scripts,
|
||||
|
||||
@@ -191,6 +191,66 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
return findings;
|
||||
},
|
||||
|
||||
// root_composition_missing_data_start
|
||||
({ rootTag }) => {
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
if (!rootTag) return findings;
|
||||
const compId = readAttr(rootTag.raw, "data-composition-id");
|
||||
if (!compId) return findings;
|
||||
const hasStart = readAttr(rootTag.raw, "data-start") !== null;
|
||||
if (!hasStart) {
|
||||
findings.push({
|
||||
code: "root_composition_missing_data_start",
|
||||
severity: "warning",
|
||||
message: `Root composition "${compId}" is missing data-start. The runtime needs data-start="0" on the root element to begin playback.`,
|
||||
fixHint: 'Add data-start="0" to the root composition element.',
|
||||
snippet: truncateSnippet(rootTag.raw),
|
||||
});
|
||||
}
|
||||
return findings;
|
||||
},
|
||||
|
||||
// standalone_composition_wrapped_in_template
|
||||
({ rawSource, options }) => {
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
if (options.isSubComposition) return findings;
|
||||
const trimmed = rawSource.trimStart().toLowerCase();
|
||||
if (trimmed.startsWith("<template")) {
|
||||
findings.push({
|
||||
code: "standalone_composition_wrapped_in_template",
|
||||
severity: "warning",
|
||||
message:
|
||||
"Root index.html is wrapped in a <template> tag. " +
|
||||
"Only sub-compositions loaded via data-composition-src should use <template> wrappers. " +
|
||||
"The runtime cannot play a standalone composition inside a template.",
|
||||
fixHint:
|
||||
"Remove the <template> wrapper. Use <!DOCTYPE html><html>...<div data-composition-id>...</div>...</html> instead.",
|
||||
});
|
||||
}
|
||||
return findings;
|
||||
},
|
||||
|
||||
// root_composition_missing_html_wrapper
|
||||
({ rawSource, options }) => {
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
if (options.isSubComposition) return findings;
|
||||
const trimmed = rawSource.trimStart().toLowerCase();
|
||||
const hasDoctype = trimmed.startsWith("<!doctype") || trimmed.startsWith("<html");
|
||||
const hasComposition = rawSource.includes("data-composition-id");
|
||||
if (hasComposition && !hasDoctype) {
|
||||
findings.push({
|
||||
code: "root_composition_missing_html_wrapper",
|
||||
severity: "warning",
|
||||
message:
|
||||
"Composition is missing <!DOCTYPE html> and <html> wrapper. " +
|
||||
"The bundler and preview expect a complete HTML document for index.html files.",
|
||||
fixHint:
|
||||
'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.',
|
||||
});
|
||||
}
|
||||
return findings;
|
||||
},
|
||||
|
||||
// requestanimationframe_in_composition
|
||||
({ scripts }) => {
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
|
||||
@@ -21,6 +21,7 @@ export type HyperframeLintResult = {
|
||||
|
||||
export type HyperframeLinterOptions = {
|
||||
filePath?: string;
|
||||
isSubComposition?: boolean;
|
||||
};
|
||||
|
||||
// A rule is a pure function: receives parsed context, returns zero or more findings.
|
||||
|
||||
Reference in New Issue
Block a user