@hyperframes/lint 0.7.24 → 0.7.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.js +90 -62
- package/dist/browser.js.map +1 -1
- package/dist/index.js +90 -62
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,10 +4,13 @@ var STYLE_BLOCK_PATTERN = /<style\b([^>]*)>([\s\S]*?)<\/style>/gi;
|
|
|
4
4
|
var SCRIPT_BLOCK_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
5
5
|
var COMPOSITION_ID_IN_CSS_PATTERN = /\[data-composition-id=["']([^"']+)["']\]/g;
|
|
6
6
|
var TIMELINE_REGISTRY_INIT_PATTERN = /window\.__timelines\s*=\s*window\.__timelines\s*\|\|\s*\{\}|window\.__timelines\s*=\s*\{\}|window\.__timelines\s*\?\?=\s*\{\}/i;
|
|
7
|
+
var TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN = /window\.__timelines\s*=\s*\{\s*(?:["'][^"']+["']|[A-Za-z_$][\w$]*)\s*:/i;
|
|
7
8
|
var TIMELINE_REGISTRY_ASSIGN_PATTERN = /window\.__timelines(?:\[[^\]]+\]|\.[A-Za-z_$][\w$]*)\s*=/i;
|
|
8
9
|
var WINDOW_TIMELINE_ASSIGN_PATTERN = /window\.__timelines(?:\[\s*["']([^"']+)["']\s*\]|\.\s*([A-Za-z_$][\w$]*))\s*=\s*([A-Za-z_$][\w$]*)/i;
|
|
9
10
|
var INVALID_SCRIPT_CLOSE_PATTERN = /<script[^>]*>[\s\S]*?<\s*\/\s*script(?!>)/i;
|
|
10
11
|
var TIMELINE_REGISTRY_KEY_PATTERN = /window\.__timelines(?:\[\s*["']([^"']+)["']\s*\]|\.\s*([A-Za-z_$][\w$]*))\s*=/g;
|
|
12
|
+
var TIMELINE_REGISTRY_OBJECT_BODY_PATTERN = /window\.__timelines\s*=\s*\{([\s\S]*?)\}/i;
|
|
13
|
+
var TIMELINE_REGISTRY_OBJECT_ENTRY_PATTERN = /(?:["']([^"']+)["']|([A-Za-z_$][\w$]*))\s*:\s*[A-Za-z_$][\w$]*/g;
|
|
11
14
|
function extractOpenTags(source) {
|
|
12
15
|
const tags = [];
|
|
13
16
|
let match;
|
|
@@ -115,6 +118,17 @@ function extractTimelineRegistryKeys(source) {
|
|
|
115
118
|
const key = match[1] ?? match[2];
|
|
116
119
|
if (key) keys.add(key);
|
|
117
120
|
}
|
|
121
|
+
const objectBody = TIMELINE_REGISTRY_OBJECT_BODY_PATTERN.exec(source)?.[1];
|
|
122
|
+
if (objectBody) {
|
|
123
|
+
const entryPattern = new RegExp(
|
|
124
|
+
TIMELINE_REGISTRY_OBJECT_ENTRY_PATTERN.source,
|
|
125
|
+
TIMELINE_REGISTRY_OBJECT_ENTRY_PATTERN.flags
|
|
126
|
+
);
|
|
127
|
+
while ((match = entryPattern.exec(objectBody)) !== null) {
|
|
128
|
+
const key = match[1] ?? match[2];
|
|
129
|
+
if (key) keys.add(key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
118
132
|
return [...keys];
|
|
119
133
|
}
|
|
120
134
|
function getInlineScriptSyntaxError(source) {
|
|
@@ -211,6 +225,9 @@ function extractScriptTextsAndSrcs(scripts) {
|
|
|
211
225
|
function isMediaTag(tagName) {
|
|
212
226
|
return tagName === "video" || tagName === "audio" || tagName === "img";
|
|
213
227
|
}
|
|
228
|
+
function hasCaptionStyles(styles) {
|
|
229
|
+
return styles.some((s) => /\.caption[-_]?(?:group|word)/i.test(s.content));
|
|
230
|
+
}
|
|
214
231
|
function truncateSnippet(value, maxLength = 220) {
|
|
215
232
|
const normalized = value.replace(/\s+/g, " ").trim();
|
|
216
233
|
if (!normalized) return void 0;
|
|
@@ -438,7 +455,7 @@ var coreRules = [
|
|
|
438
455
|
return [];
|
|
439
456
|
}
|
|
440
457
|
const findings = [];
|
|
441
|
-
if (!TIMELINE_REGISTRY_INIT_PATTERN.test(source) && !TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source)) {
|
|
458
|
+
if (!TIMELINE_REGISTRY_INIT_PATTERN.test(source) && !TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source) && !TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(source)) {
|
|
442
459
|
findings.push({
|
|
443
460
|
code: "missing_timeline_registry",
|
|
444
461
|
severity: "error",
|
|
@@ -1353,13 +1370,16 @@ function findContainingCompositionId(tag, ranges) {
|
|
|
1353
1370
|
}
|
|
1354
1371
|
return match?.id || null;
|
|
1355
1372
|
}
|
|
1373
|
+
function getClipTagClasses(tag) {
|
|
1374
|
+
const classAttr = readAttr(tag.raw, "class") || "";
|
|
1375
|
+
const classes = classAttr.split(/\s+/).filter(Boolean);
|
|
1376
|
+
return classes.includes("clip") ? { classAttr, classes } : null;
|
|
1377
|
+
}
|
|
1356
1378
|
function collectClipStartBoundariesByComposition(source, tags) {
|
|
1357
1379
|
const ranges = collectCompositionRanges(source, tags);
|
|
1358
1380
|
const boundaries = /* @__PURE__ */ new Map();
|
|
1359
1381
|
for (const tag of tags) {
|
|
1360
|
-
|
|
1361
|
-
const classes = classAttr.split(/\s+/).filter(Boolean);
|
|
1362
|
-
if (!classes.includes("clip")) continue;
|
|
1382
|
+
if (!getClipTagClasses(tag)) continue;
|
|
1363
1383
|
const compositionId = findContainingCompositionId(tag, ranges);
|
|
1364
1384
|
if (!compositionId) continue;
|
|
1365
1385
|
const start = numberValue(readAttr(tag.raw, "data-start") ?? void 0);
|
|
@@ -1516,6 +1536,23 @@ function extractStandaloneGsapTransformCalls(script) {
|
|
|
1516
1536
|
}
|
|
1517
1537
|
return calls;
|
|
1518
1538
|
}
|
|
1539
|
+
function scanScriptsForRegexMatches(scripts, pattern, options) {
|
|
1540
|
+
const hits = [];
|
|
1541
|
+
for (const script of scripts) {
|
|
1542
|
+
const content = options.stripComments ? stripJsComments(script.content) : script.content;
|
|
1543
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
1544
|
+
let match;
|
|
1545
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1546
|
+
const contextStart = Math.max(0, match.index - options.contextBefore);
|
|
1547
|
+
const contextEnd = Math.min(
|
|
1548
|
+
content.length,
|
|
1549
|
+
match.index + match[0].length + options.contextAfter
|
|
1550
|
+
);
|
|
1551
|
+
hits.push({ match, snippet: content.slice(contextStart, contextEnd) });
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return hits;
|
|
1555
|
+
}
|
|
1519
1556
|
var gsapRules = [
|
|
1520
1557
|
// overlapping_gsap_tweens + gsap_animates_clip_element + unscoped_gsap_selector
|
|
1521
1558
|
// fallow-ignore-next-line complexity
|
|
@@ -1524,17 +1561,16 @@ var gsapRules = [
|
|
|
1524
1561
|
const clipIds = /* @__PURE__ */ new Map();
|
|
1525
1562
|
const clipClasses = /* @__PURE__ */ new Map();
|
|
1526
1563
|
for (const tag of tags) {
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
if (!classes.includes("clip")) continue;
|
|
1564
|
+
const clipTag = getClipTagClasses(tag);
|
|
1565
|
+
if (!clipTag) continue;
|
|
1530
1566
|
const id = readAttr(tag.raw, "id");
|
|
1531
1567
|
const info = {
|
|
1532
1568
|
tag: tag.name,
|
|
1533
1569
|
id: id || "",
|
|
1534
|
-
classes: classAttr
|
|
1570
|
+
classes: clipTag.classAttr
|
|
1535
1571
|
};
|
|
1536
1572
|
if (id) clipIds.set(`#${id}`, info);
|
|
1537
|
-
for (const cls of classes) {
|
|
1573
|
+
for (const cls of clipTag.classes) {
|
|
1538
1574
|
if (cls !== "clip") clipClasses.set(`.${cls}`, info);
|
|
1539
1575
|
}
|
|
1540
1576
|
}
|
|
@@ -1782,8 +1818,7 @@ ${right.raw}`)
|
|
|
1782
1818
|
// fallow-ignore-next-line complexity
|
|
1783
1819
|
({ scripts, styles }) => {
|
|
1784
1820
|
const findings = [];
|
|
1785
|
-
|
|
1786
|
-
if (!isCaptionFile) return findings;
|
|
1821
|
+
if (!hasCaptionStyles(styles)) return findings;
|
|
1787
1822
|
for (const script of scripts) {
|
|
1788
1823
|
const content = script.content;
|
|
1789
1824
|
const hasAudioData = /AUDIO|audio[-_]?data|bands\[/.test(content);
|
|
@@ -1808,44 +1843,38 @@ ${right.raw}`)
|
|
|
1808
1843
|
// gsap_infinite_repeat
|
|
1809
1844
|
({ scripts }) => {
|
|
1810
1845
|
const findings = [];
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
snippet: truncateSnippet(snippet)
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1846
|
+
const pattern = /repeat\s*:\s*-1(?!\d)/g;
|
|
1847
|
+
for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
1848
|
+
stripComments: true,
|
|
1849
|
+
contextBefore: 60,
|
|
1850
|
+
contextAfter: 60
|
|
1851
|
+
})) {
|
|
1852
|
+
findings.push({
|
|
1853
|
+
code: "gsap_infinite_repeat",
|
|
1854
|
+
severity: "error",
|
|
1855
|
+
message: "GSAP tween uses `repeat: -1` (infinite). Infinite repeats break the deterministic capture engine which seeks to exact frame times. Use a finite repeat count calculated from the composition duration: `repeat: Math.floor(duration / cycleDuration) - 1`.",
|
|
1856
|
+
fixHint: "Replace `repeat: -1` with a finite count, e.g. `repeat: Math.floor(totalDuration / singleCycleDuration) - 1`. Use Math.floor (not Math.ceil) to ensure the animation fits within the total duration.",
|
|
1857
|
+
snippet: truncateSnippet(snippet)
|
|
1858
|
+
});
|
|
1827
1859
|
}
|
|
1828
1860
|
return findings;
|
|
1829
1861
|
},
|
|
1830
1862
|
// gsap_repeat_ceil_overshoot
|
|
1831
1863
|
({ scripts }) => {
|
|
1832
1864
|
const findings = [];
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
snippet: truncateSnippet(snippet)
|
|
1847
|
-
});
|
|
1848
|
-
}
|
|
1865
|
+
const pattern = /repeat\s*:\s*Math\.ceil\s*\([^)]+\)\s*-\s*1/g;
|
|
1866
|
+
for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
1867
|
+
stripComments: false,
|
|
1868
|
+
contextBefore: 40,
|
|
1869
|
+
contextAfter: 40
|
|
1870
|
+
})) {
|
|
1871
|
+
findings.push({
|
|
1872
|
+
code: "gsap_repeat_ceil_overshoot",
|
|
1873
|
+
severity: "warning",
|
|
1874
|
+
message: "GSAP repeat calculation uses `Math.ceil` which can overshoot the composition duration. For example, Math.ceil(10.5 / 2) - 1 = 5 repeats \u2192 6 cycles \xD7 2s = 12s, exceeding 10.5s.",
|
|
1875
|
+
fixHint: "Use `Math.floor` instead of `Math.ceil` to ensure the animation fits within the duration: `repeat: Math.floor(totalDuration / cycleDuration) - 1`. Math.floor(10.5 / 2) - 1 = 4 repeats \u2192 5 cycles \xD7 2s = 10s \u2713",
|
|
1876
|
+
snippet: truncateSnippet(snippet)
|
|
1877
|
+
});
|
|
1849
1878
|
}
|
|
1850
1879
|
return findings;
|
|
1851
1880
|
},
|
|
@@ -1886,7 +1915,7 @@ ${right.raw}`)
|
|
|
1886
1915
|
for (const script of scripts) {
|
|
1887
1916
|
const content = script.content;
|
|
1888
1917
|
if (!/gsap\.timeline/.test(content)) continue;
|
|
1889
|
-
const hasRegistration = WINDOW_TIMELINE_ASSIGN_PATTERN.test(content);
|
|
1918
|
+
const hasRegistration = WINDOW_TIMELINE_ASSIGN_PATTERN.test(content) || TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(content);
|
|
1890
1919
|
if (hasRegistration || canInheritFromHost) continue;
|
|
1891
1920
|
findings.push({
|
|
1892
1921
|
code: "gsap_timeline_not_registered",
|
|
@@ -1971,23 +2000,21 @@ ${right.raw}`)
|
|
|
1971
2000
|
// gsap_group_selector_keyframes
|
|
1972
2001
|
({ scripts }) => {
|
|
1973
2002
|
const findings = [];
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
});
|
|
1990
|
-
}
|
|
2003
|
+
const pattern = /\.(?:to|from|fromTo)\(\s*["']([^"']+,\s*[^"']+)["']\s*,\s*\{[^}]*keyframes/g;
|
|
2004
|
+
for (const { match, snippet } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
2005
|
+
stripComments: true,
|
|
2006
|
+
contextBefore: 20,
|
|
2007
|
+
contextAfter: 40
|
|
2008
|
+
})) {
|
|
2009
|
+
const selector = match[1];
|
|
2010
|
+
const count = selector.split(",").length;
|
|
2011
|
+
findings.push({
|
|
2012
|
+
code: "gsap_group_selector_keyframes",
|
|
2013
|
+
severity: "warning",
|
|
2014
|
+
message: `GSAP tween targets ${count} elements with shared keyframes ("${truncateSnippet(selector, 60)}"). Editing one element's keyframes in Studio will affect all ${count} elements. Split into individual tweens for per-element keyframe control.`,
|
|
2015
|
+
fixHint: `Replace the group selector with individual tl.to() calls per element, each with their own keyframes object.`,
|
|
2016
|
+
snippet: truncateSnippet(snippet)
|
|
2017
|
+
});
|
|
1991
2018
|
}
|
|
1992
2019
|
return findings;
|
|
1993
2020
|
}
|
|
@@ -2208,6 +2235,7 @@ import { COMPOSITION_VARIABLE_TYPES } from "@hyperframes/parsers/composition";
|
|
|
2208
2235
|
var MAX_COMPOSITION_LINES = 300;
|
|
2209
2236
|
var MAX_TIMED_ELEMENTS_PER_TRACK = 3;
|
|
2210
2237
|
var TRACK_DENSITY_EXEMPT_TAGS = /* @__PURE__ */ new Set(["audio", "script", "style", "video"]);
|
|
2238
|
+
var OVERLAP_EPSILON_SECONDS = 1e-6;
|
|
2211
2239
|
function countPhysicalLines(source) {
|
|
2212
2240
|
if (source.length === 0) return 0;
|
|
2213
2241
|
const normalized = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
@@ -2520,7 +2548,7 @@ var compositionRules = [
|
|
|
2520
2548
|
const current = clips[i];
|
|
2521
2549
|
const next = clips[i + 1];
|
|
2522
2550
|
if (!current || !next) continue;
|
|
2523
|
-
if (current.end
|
|
2551
|
+
if (current.end - next.start > OVERLAP_EPSILON_SECONDS) {
|
|
2524
2552
|
findings.push({
|
|
2525
2553
|
code: "overlapping_clips_same_track",
|
|
2526
2554
|
severity: "error",
|