mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-02 19:57:13 +00:00
fix(lint): a timeline registered in a local script file counts as registered (#6829)
* fix(lint): a timeline registered in a local script file counts as registered * fix(lint): a module script file is read as a module * style(lint): format the module script check
This commit is contained in:
@@ -64,7 +64,17 @@ export function buildLintContext(html: string, options: HyperframeLinterOptions
|
||||
rootRelativePath: style.rootRelativePath,
|
||||
})),
|
||||
];
|
||||
const scripts = structure.scripts;
|
||||
const scripts = [
|
||||
...structure.scripts,
|
||||
...(options.externalScripts ?? []).map((script) => ({
|
||||
attrs: script.attrs,
|
||||
content: script.content,
|
||||
raw: script.content,
|
||||
index: -1,
|
||||
file: script.file,
|
||||
rootRelativePath: script.rootRelativePath,
|
||||
})),
|
||||
];
|
||||
const compositionIds = collectCompositionIds(tags);
|
||||
const rootTag = findRootTag(source, tags);
|
||||
const rootCompositionId = readDecodedAttr(rootTag?.raw || "", "data-composition-id");
|
||||
|
||||
@@ -566,6 +566,46 @@ describe("template shell style sources", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("local script files", () => {
|
||||
const registryFindings = async (project: string) =>
|
||||
(await lintProject(project)).results
|
||||
.flatMap((entry) => entry.result.findings)
|
||||
.filter((finding) => finding.code === "missing_timeline_registry");
|
||||
|
||||
function projectWithScript(script: string, attrs = ""): string {
|
||||
const project = makeProject(
|
||||
`<html><body>
|
||||
<div data-composition-id="main" data-width="1920" data-height="1080" data-start="0" data-duration="10"></div>
|
||||
<script ${attrs} src="animation.js"></script>
|
||||
</body></html>`,
|
||||
);
|
||||
writeFileSync(join(project, "animation.js"), script);
|
||||
return project;
|
||||
}
|
||||
|
||||
it("counts a timeline registered in a local script file", async () => {
|
||||
const registered = projectWithScript(
|
||||
'window.__timelines = window.__timelines || {};\nwindow.__timelines["main"] = gsap.timeline({ paused: true });',
|
||||
);
|
||||
expect(await registryFindings(registered)).toEqual([]);
|
||||
expect(await registryFindings(projectWithScript("console.log('no timeline')"))).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("reads a module script file as a module", async () => {
|
||||
const project = projectWithScript(
|
||||
'import { gsap } from "gsap";\nwindow.__timelines = window.__timelines || {};\nwindow.__timelines["main"] = gsap.timeline({ paused: true });',
|
||||
'type="module"',
|
||||
);
|
||||
|
||||
const codes = (await lintProject(project)).results
|
||||
.flatMap((entry) => entry.result.findings)
|
||||
.map((finding) => finding.code);
|
||||
|
||||
expect(codes).not.toContain("invalid_inline_script_syntax");
|
||||
expect(codes).not.toContain("missing_timeline_registry");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sub-composition stylesheet links", () => {
|
||||
function projectWithSharedFonts(compDir: string, href: string): string {
|
||||
const project = makeProject(validHtml());
|
||||
|
||||
@@ -155,6 +155,42 @@ export function collectExternalStyles(
|
||||
return styles;
|
||||
}
|
||||
|
||||
const MAX_EXTERNAL_SCRIPT_BYTES = 1_000_000;
|
||||
|
||||
function collectExternalScripts(
|
||||
projectDir: string,
|
||||
html: string,
|
||||
compSrcPath?: string,
|
||||
): NonNullable<HyperframeLinterOptions["externalScripts"]> {
|
||||
const scripts: NonNullable<HyperframeLinterOptions["externalScripts"]> = [];
|
||||
const { document } = parseHTML(html);
|
||||
for (const element of querySelectorAllIncludingTemplates(document, "script[src]")) {
|
||||
const src = element.getAttribute("src") ?? "";
|
||||
if (!isLocalStylesheetHref(src) || /\.min\.m?js(?:[?#]|$)/i.test(src)) continue;
|
||||
const path = src.split(/[?#]/)[0] ?? src;
|
||||
const besideFile = compSrcPath ? join(dirname(compSrcPath), path) : path;
|
||||
const script =
|
||||
resolveExistingLocalAsset(projectDir, besideFile) ??
|
||||
(compSrcPath && staysInsideProjectRoot(path)
|
||||
? resolveExistingLocalAsset(projectDir, path)
|
||||
: null);
|
||||
if (!script) continue;
|
||||
const read = readProjectFile(script.resolved);
|
||||
if (read.kind !== "file" || read.text.length > MAX_EXTERNAL_SCRIPT_BYTES) continue;
|
||||
const attrs = Array.from(element.attributes)
|
||||
.map(({ name, value }) => `${name}="${value.replace(/&/g, "&").replace(/"/g, """)}"`)
|
||||
.join(" ");
|
||||
scripts.push({
|
||||
src,
|
||||
attrs,
|
||||
content: read.text,
|
||||
file: join(projectDir, script.rootRelativePath),
|
||||
rootRelativePath: script.rootRelativePath,
|
||||
});
|
||||
}
|
||||
return scripts;
|
||||
}
|
||||
|
||||
function collectCssSources(projectDir: string, html: string, compSrcPath?: string): CssSource[] {
|
||||
const sources: CssSource[] = [];
|
||||
const { document } = parseHTML(html);
|
||||
@@ -268,6 +304,7 @@ export async function lintProject(
|
||||
compSrcPath: rootFile,
|
||||
externalStyles: collectExternalStyles(projectDir, rootHtml, rootCompSrcPath),
|
||||
projectFontFamilies,
|
||||
externalScripts: collectExternalScripts(projectDir, rootHtml, rootCompSrcPath),
|
||||
});
|
||||
results.push({ file: rootFile, result: rootResult, contentHash: contentDigest(rootHtml) });
|
||||
totalErrors += rootResult.errorCount;
|
||||
@@ -311,6 +348,7 @@ export async function lintProject(
|
||||
compSrcPath,
|
||||
externalStyles: collectExternalStyles(projectDir, html, compSrcPath),
|
||||
projectFontFamilies,
|
||||
externalScripts: collectExternalScripts(projectDir, html, compSrcPath),
|
||||
});
|
||||
results.push({
|
||||
file: `compositions/${file}`,
|
||||
|
||||
@@ -626,10 +626,14 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
}
|
||||
if (/(?:^|\s)data-no-timeline(?=[\s=/]|$)/i.test(rootTag?.attrs || "")) return [];
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
const registered = [source, ...(options.externalScripts ?? []).map((script) => script.content)];
|
||||
if (
|
||||
!TIMELINE_REGISTRY_INIT_PATTERN.test(source) &&
|
||||
!TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source) &&
|
||||
!TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(source)
|
||||
!registered.some(
|
||||
(text) =>
|
||||
TIMELINE_REGISTRY_INIT_PATTERN.test(text) ||
|
||||
TIMELINE_REGISTRY_ASSIGN_PATTERN.test(text) ||
|
||||
TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(text),
|
||||
)
|
||||
) {
|
||||
findings.push({
|
||||
code: "missing_timeline_registry",
|
||||
@@ -839,9 +843,9 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
({ scripts, locate }) => {
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
for (const script of scripts) {
|
||||
const type = readDecodedAttr(script.raw, "type")?.trim().toLowerCase() ?? "";
|
||||
if (readDecodedAttr(script.raw, "src") !== null || SCRIPT_SYNTAX_EXEMPT_TYPES.has(type))
|
||||
continue;
|
||||
const tag = `<script ${script.attrs}>`;
|
||||
const type = readDecodedAttr(tag, "type")?.trim().toLowerCase() ?? "";
|
||||
if (readDecodedAttr(tag, "src") !== null || SCRIPT_SYNTAX_EXEMPT_TYPES.has(type)) continue;
|
||||
const syntaxError = getInlineScriptSyntaxError(script.content);
|
||||
if (!syntaxError) continue;
|
||||
findings.push({
|
||||
|
||||
@@ -47,6 +47,13 @@ export type HyperframeLinterOptions = {
|
||||
file?: string;
|
||||
rootRelativePath?: string;
|
||||
}>;
|
||||
externalScripts?: Array<{
|
||||
src: string;
|
||||
attrs: string;
|
||||
content: string;
|
||||
file: string;
|
||||
rootRelativePath: string;
|
||||
}>;
|
||||
/**
|
||||
* Set to `true` when linting compositions destined for distributed / Lambda
|
||||
* rendering, where system-font capture (`allowSystemFontCapture`) is
|
||||
|
||||
Reference in New Issue
Block a user