diff --git a/packages/cli/src/utils/lintProject.test.ts b/packages/cli/src/utils/lintProject.test.ts index 0ce2eb0c67..bb00a864b5 100644 --- a/packages/cli/src/utils/lintProject.test.ts +++ b/packages/cli/src/utils/lintProject.test.ts @@ -71,6 +71,26 @@ describe("lintProject", () => { expect(first?.file).toBe("index.html"); }); + it("detects timeline registration in a local external script", async () => { + const projectDir = makeProject(`
+ + + `); + const workDir = join(projectDir, "work"); + mkdirSync(workDir, { recursive: true }); + writeFileSync( + join(workDir, "dance.js"), + `window.__timelines = window.__timelines || {}; + window.__timelines["dance"] = gsap.timeline({ paused: true });`, + ); + + const { results } = await lintProject(projectDir); + + expect( + results[0]?.result.findings.find((finding) => finding.code === "missing_timeline_registry"), + ).toBeUndefined(); + }); + it("detects errors in index.html", async () => { const project = makeProject(htmlWithMissingMediaId()); const { totalErrors, results } = await lintProject(project); diff --git a/packages/lint/src/context.ts b/packages/lint/src/context.ts index 9638f3ac52..74b533fadb 100644 --- a/packages/lint/src/context.ts +++ b/packages/lint/src/context.ts @@ -63,7 +63,15 @@ export function buildLintContext(html: string, options: HyperframeLinterOptions index: -1, })), ]; - const scripts = structure.scripts; + const scripts = [ + ...structure.scripts, + ...(options.externalScripts ?? []).map((script) => ({ + attrs: `src="${script.src}"`, + content: script.content, + raw: script.content, + index: -1, + })), + ]; const compositionIds = collectCompositionIds(tags); const rootTag = findRootTag(source, tags); const rootCompositionId = readAttr(rootTag?.raw || "", "data-composition-id"); diff --git a/packages/lint/src/project.ts b/packages/lint/src/project.ts index 52d828c1db..2578d40347 100644 --- a/packages/lint/src/project.ts +++ b/packages/lint/src/project.ts @@ -88,6 +88,24 @@ function collectExternalStyles( return styles; } +function collectExternalScripts( + projectDir: string, + html: string, + compSrcPath?: string, +): Array<{ src: string; content: string }> { + const scripts: Array<{ src: string; content: string }> = []; + const { document } = parseHTML(html); + for (const script of querySelectorAllIncludingTemplates(document, "script[src]")) { + const src = script.getAttribute("src") ?? ""; + if (!src || isRemoteOrInlineUrl(src)) continue; + const rootRelative = compSrcPath ? join(dirname(compSrcPath), src) : src; + const asset = resolveExistingLocalAsset(projectDir, rootRelative); + if (!asset) continue; + scripts.push({ src, content: readFileSync(asset.resolved, "utf-8") }); + } + return scripts; +} + function collectCssSources(projectDir: string, html: string, compSrcPath?: string): CssSource[] { const sources: CssSource[] = []; const { document } = parseHTML(html); @@ -191,6 +209,7 @@ export async function lintProject(projectDir: string): Promise