Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/cli/src/utils/lintProject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`<html><body>
<div data-composition-id="dance" data-width="1920" data-height="1080" data-start="0" data-duration="10"></div>
<script src="work/dance.js"></script>
</body></html>`);
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);
Expand Down
10 changes: 9 additions & 1 deletion packages/lint/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
20 changes: 20 additions & 0 deletions packages/lint/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -191,6 +209,7 @@ export async function lintProject(projectDir: string): Promise<ProjectLintResult
const rootResult = await lintHyperframeHtml(rootHtml, {
filePath: indexPath,
externalStyles: collectExternalStyles(projectDir, rootHtml),
externalScripts: collectExternalScripts(projectDir, rootHtml),
});
results.push({ file: "index.html", result: rootResult });
totalErrors += rootResult.errorCount;
Expand Down Expand Up @@ -226,6 +245,7 @@ export async function lintProject(projectDir: string): Promise<ProjectLintResult
filePath,
isSubComposition: true,
externalStyles: collectExternalStyles(projectDir, html, compSrcPath),
externalScripts: collectExternalScripts(projectDir, html, compSrcPath),
});
results.push({ file: `compositions/${file}`, result });
totalErrors += result.errorCount;
Expand Down
18 changes: 10 additions & 8 deletions packages/lint/src/rules/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,16 +264,17 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
},

// missing_timeline_registry + timeline_registry_missing_init
({ source, rawSource, options }) => {
({ source, rawSource, scripts, options }) => {
// Sub-compositions inherit window.__timelines from the host composition
if (options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template")) {
return [];
}
const findings: HyperframeLintFinding[] = [];
const timelineSource = [source, ...scripts.map((script) => script.content)].join("\n");
if (
!TIMELINE_REGISTRY_INIT_PATTERN.test(source) &&
!TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source) &&
!TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(source)
!TIMELINE_REGISTRY_INIT_PATTERN.test(timelineSource) &&
!TIMELINE_REGISTRY_ASSIGN_PATTERN.test(timelineSource) &&
!TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(timelineSource)
) {
findings.push({
code: "missing_timeline_registry",
Expand All @@ -283,8 +284,8 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
});
}
if (
TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source) &&
!TIMELINE_REGISTRY_INIT_PATTERN.test(source)
TIMELINE_REGISTRY_ASSIGN_PATTERN.test(timelineSource) &&
!TIMELINE_REGISTRY_INIT_PATTERN.test(timelineSource)
) {
findings.push({
code: "timeline_registry_missing_init",
Expand All @@ -299,7 +300,7 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
},

// timeline_id_mismatch
({ source }) => {
({ source, scripts }) => {
const findings: HyperframeLintFinding[] = [];
const htmlCompIds = new Set<string>();
const timelineRegKeys = new Set<string>();
Expand All @@ -308,7 +309,8 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
while ((m = compIdRe.exec(source)) !== null) {
if (m[1]) htmlCompIds.add(m[1]);
}
for (const key of extractTimelineRegistryKeys(source)) {
const timelineSource = [source, ...scripts.map((script) => script.content)].join("\n");
for (const key of extractTimelineRegistryKeys(timelineSource)) {
timelineRegKeys.add(key);
}
for (const key of timelineRegKeys) {
Expand Down
1 change: 1 addition & 0 deletions packages/lint/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type HyperframeLinterOptions = {
filePath?: string;
isSubComposition?: boolean;
externalStyles?: Array<{ href: string; content: string }>;
externalScripts?: Array<{ src: string; content: string }>;
/**
* Set to `true` when linting compositions destined for distributed / Lambda
* rendering, where system-font capture (`allowSystemFontCapture`) is
Expand Down
Loading