@hyperframes/lint 0.8.37 → 0.8.39
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 +16 -93
- package/dist/browser.js.map +1 -1
- package/dist/index.js +16 -93
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -14,12 +14,24 @@ function parseHtmlStructure(source) {
|
|
|
14
14
|
const blocks = { script: [], style: [] };
|
|
15
15
|
const openTagsByName = /* @__PURE__ */ new Map();
|
|
16
16
|
const openBlocks = [];
|
|
17
|
+
let explicitOpenTag = null;
|
|
17
18
|
const parser = new Parser(
|
|
18
19
|
{
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
onopentagname(name) {
|
|
21
|
+
let tokenStart = parser.endIndex - 1;
|
|
22
|
+
while (tokenStart >= parser.startIndex && !/[\t\n\f\r />]/.test(source.charAt(tokenStart))) {
|
|
23
|
+
tokenStart -= 1;
|
|
24
|
+
}
|
|
25
|
+
const index = source.indexOf("<", tokenStart + 1);
|
|
26
|
+
explicitOpenTag = index >= 0 && index < parser.endIndex && source.slice(index + 1, parser.endIndex).toLowerCase() === name ? { index, nameEnd: parser.endIndex } : null;
|
|
27
|
+
},
|
|
28
|
+
onopentag(name, _attrs, isImplied) {
|
|
29
|
+
const origin = !isImplied ? explicitOpenTag : null;
|
|
30
|
+
const index = origin?.index ?? parser.startIndex;
|
|
31
|
+
explicitOpenTag = null;
|
|
21
32
|
const raw = source.slice(index, parser.endIndex + 1);
|
|
22
|
-
const
|
|
33
|
+
const rawAttrs = origin ? source.slice(origin.nameEnd, parser.endIndex) : raw.slice(name.length + 1, -1);
|
|
34
|
+
const attrs = rawAttrs.replace(/\s*\/$/, "");
|
|
23
35
|
const tag = { raw, name, attrs, index };
|
|
24
36
|
tags.push(tag);
|
|
25
37
|
const sameNameStack = openTagsByName.get(name) ?? [];
|
|
@@ -2843,10 +2855,6 @@ function targetHasNoStableIdentity(selector, identity) {
|
|
|
2843
2855
|
if (identity) return false;
|
|
2844
2856
|
return selector === UNRESOLVED_TARGET || selector === "dwell/hold" || selector.startsWith("proxy \u2192 ");
|
|
2845
2857
|
}
|
|
2846
|
-
function readRegisteredTimelineCompositionId(script) {
|
|
2847
|
-
const match = script.match(WINDOW_TIMELINE_ASSIGN_PATTERN);
|
|
2848
|
-
return match?.[1] || match?.[2] || null;
|
|
2849
|
-
}
|
|
2850
2858
|
function unwrapRaw(value) {
|
|
2851
2859
|
if (typeof value === "number") return value;
|
|
2852
2860
|
if (typeof value !== "string") return void 0;
|
|
@@ -3014,21 +3022,6 @@ function makesOverlayVisible(win) {
|
|
|
3014
3022
|
if (win.method === "from" && isHiddenGsapState(win.propertyValues)) return true;
|
|
3015
3023
|
return isVisibleGsapState(win.propertyValues);
|
|
3016
3024
|
}
|
|
3017
|
-
function isSceneBoundaryExit(win) {
|
|
3018
|
-
if (win.end <= win.position) return false;
|
|
3019
|
-
if (win.method !== "to" && win.method !== "fromTo") return false;
|
|
3020
|
-
return isHiddenGsapState(win.propertyValues);
|
|
3021
|
-
}
|
|
3022
|
-
function isHardKillSet(win, selector, boundary) {
|
|
3023
|
-
return win.method === "set" && win.targetSelector === selector && Math.abs(win.position - boundary) <= SCENE_BOUNDARY_EPSILON_SECONDS && isHiddenGsapState(win.propertyValues);
|
|
3024
|
-
}
|
|
3025
|
-
function hiddenStateLiteral(values) {
|
|
3026
|
-
if (zeroValue(values.autoAlpha)) return "{ autoAlpha: 0 }";
|
|
3027
|
-
if (zeroValue(values.opacity)) return "{ opacity: 0 }";
|
|
3028
|
-
if (stringValue(values.visibility)?.toLowerCase() === "hidden") return '{ visibility: "hidden" }';
|
|
3029
|
-
if (stringValue(values.display)?.toLowerCase() === "none") return '{ display: "none" }';
|
|
3030
|
-
return "{ opacity: 0 }";
|
|
3031
|
-
}
|
|
3032
3025
|
function findTagEnd(source, tag) {
|
|
3033
3026
|
const escapedTagName = tag.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3034
3027
|
const pattern = new RegExp(`<\\/?${escapedTagName}\\b[^>]*>`, "gi");
|
|
@@ -3045,56 +3038,11 @@ function findTagEnd(source, tag) {
|
|
|
3045
3038
|
}
|
|
3046
3039
|
return source.length;
|
|
3047
3040
|
}
|
|
3048
|
-
function collectCompositionRanges(source, tags) {
|
|
3049
|
-
return tags.map((tag) => {
|
|
3050
|
-
const id = readDecodedAttr(tag.raw, "data-composition-id");
|
|
3051
|
-
if (!id) return null;
|
|
3052
|
-
return {
|
|
3053
|
-
id,
|
|
3054
|
-
start: tag.index,
|
|
3055
|
-
end: findTagEnd(source, tag)
|
|
3056
|
-
};
|
|
3057
|
-
}).filter((range) => range !== null);
|
|
3058
|
-
}
|
|
3059
|
-
function findContainingCompositionId(tag, ranges) {
|
|
3060
|
-
let match = null;
|
|
3061
|
-
for (const range of ranges) {
|
|
3062
|
-
if (tag.index < range.start || tag.index >= range.end) continue;
|
|
3063
|
-
if (!match || range.start >= match.start) match = range;
|
|
3064
|
-
}
|
|
3065
|
-
return match?.id || null;
|
|
3066
|
-
}
|
|
3067
3041
|
function getClipTagClasses(tag) {
|
|
3068
3042
|
const classAttr = readAttr(tag.raw, "class") || "";
|
|
3069
3043
|
const classes = classAttr.split(/\s+/).filter(Boolean);
|
|
3070
3044
|
return classes.includes("clip") ? { classAttr, classes } : null;
|
|
3071
3045
|
}
|
|
3072
|
-
function collectClipStartBoundariesByComposition(source, tags) {
|
|
3073
|
-
const ranges = collectCompositionRanges(source, tags);
|
|
3074
|
-
const boundaries = /* @__PURE__ */ new Map();
|
|
3075
|
-
for (const tag of tags) {
|
|
3076
|
-
if (!getClipTagClasses(tag)) continue;
|
|
3077
|
-
const compositionId = findContainingCompositionId(tag, ranges);
|
|
3078
|
-
if (!compositionId) continue;
|
|
3079
|
-
const start = numberValue(readAttr(tag.raw, "data-start") ?? void 0);
|
|
3080
|
-
if (start == null || start <= 0) continue;
|
|
3081
|
-
const compositionBoundaries = boundaries.get(compositionId) ?? /* @__PURE__ */ new Set();
|
|
3082
|
-
compositionBoundaries.add(start);
|
|
3083
|
-
boundaries.set(compositionId, compositionBoundaries);
|
|
3084
|
-
}
|
|
3085
|
-
return new Map(
|
|
3086
|
-
[...boundaries.entries()].map(([compositionId, values]) => [
|
|
3087
|
-
compositionId,
|
|
3088
|
-
[...values].sort((a, b) => a - b)
|
|
3089
|
-
])
|
|
3090
|
-
);
|
|
3091
|
-
}
|
|
3092
|
-
function findMatchingSceneBoundary(time, boundaries) {
|
|
3093
|
-
for (const boundary of boundaries) {
|
|
3094
|
-
if (Math.abs(time - boundary) <= SCENE_BOUNDARY_EPSILON_SECONDS) return boundary;
|
|
3095
|
-
}
|
|
3096
|
-
return null;
|
|
3097
|
-
}
|
|
3098
3046
|
function readStyleProperty(style, property) {
|
|
3099
3047
|
const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3100
3048
|
const match = style.match(new RegExp(`(?:^|;)\\s*${escapedProperty}\\s*:\\s*([^;]+)`, "i"));
|
|
@@ -3593,7 +3541,7 @@ var gsapRules = [
|
|
|
3593
3541
|
},
|
|
3594
3542
|
// overlapping_gsap_tweens + gsap_animates_clip_element
|
|
3595
3543
|
// fallow-ignore-next-line complexity
|
|
3596
|
-
async ({
|
|
3544
|
+
async ({ tags, scripts, styles }) => {
|
|
3597
3545
|
const findings = [];
|
|
3598
3546
|
const authoredHiddenSelectors = new Set(
|
|
3599
3547
|
scripts.flatMap((script) => [...extractStandaloneHiddenSelectors(script.content)])
|
|
@@ -3614,13 +3562,10 @@ var gsapRules = [
|
|
|
3614
3562
|
if (cls !== "clip") clipClasses.set(`.${cls}`, info);
|
|
3615
3563
|
}
|
|
3616
3564
|
}
|
|
3617
|
-
const clipStartBoundariesByComposition = collectClipStartBoundariesByComposition(source, tags);
|
|
3618
3565
|
const styleRules = collectSimpleStyleRules(styles);
|
|
3619
3566
|
const reportedVisibleOverlayKeys = /* @__PURE__ */ new Set();
|
|
3620
3567
|
for (const script of scripts) {
|
|
3621
|
-
const localTimelineCompId = readRegisteredTimelineCompositionId(script.content);
|
|
3622
3568
|
const gsapWindows = await cachedExtractGsapWindows(script.content);
|
|
3623
|
-
const clipStartBoundaries = clipStartBoundariesByComposition.get(localTimelineCompId || rootCompositionId || "") ?? [];
|
|
3624
3569
|
for (let i = 0; i < gsapWindows.length; i++) {
|
|
3625
3570
|
const left = gsapWindows[i];
|
|
3626
3571
|
if (!left) continue;
|
|
@@ -3682,28 +3627,6 @@ ${right.raw}`)
|
|
|
3682
3627
|
snippet: truncateSnippet(fromToWindows.map((win) => win.raw).join("\n"))
|
|
3683
3628
|
});
|
|
3684
3629
|
}
|
|
3685
|
-
if (clipStartBoundaries.length > 0) {
|
|
3686
|
-
for (const win of gsapWindows) {
|
|
3687
|
-
if (win.targetSelector === UNRESOLVED_TARGET) continue;
|
|
3688
|
-
if (!isSceneBoundaryExit(win)) continue;
|
|
3689
|
-
const boundary = findMatchingSceneBoundary(win.end, clipStartBoundaries);
|
|
3690
|
-
if (boundary == null) continue;
|
|
3691
|
-
const hasHardKill = gsapWindows.some(
|
|
3692
|
-
(candidate) => isHardKillSet(candidate, win.targetSelector, boundary)
|
|
3693
|
-
);
|
|
3694
|
-
if (hasHardKill) continue;
|
|
3695
|
-
const exitClipInfo = clipIds.get(win.targetSelector) || clipClasses.get(win.targetSelector);
|
|
3696
|
-
const fixHint = exitClipInfo ? `"${win.targetSelector}" is a clip element \u2014 the framework already manages its visibility. Wrap the scene's content in an inner non-clip <div>, move the exit tween and the hard kill (\`tl.set("<inner-selector>", ${hiddenStateLiteral(win.propertyValues)}, ${boundary.toFixed(2)})\`) onto that wrapper instead.` : `Add \`tl.set("${win.targetSelector}", ${hiddenStateLiteral(win.propertyValues)}, ${boundary.toFixed(2)})\` after the exit tween.`;
|
|
3697
|
-
findings.push({
|
|
3698
|
-
code: "gsap_exit_missing_hard_kill",
|
|
3699
|
-
severity: "error",
|
|
3700
|
-
message: `GSAP exit on "${win.targetSelector}" ends at the ${boundary.toFixed(2)}s clip start boundary without a matching tl.set hard kill. Non-linear seeking can land after the fade and leave stale visibility state.`,
|
|
3701
|
-
selector: win.targetSelector,
|
|
3702
|
-
fixHint,
|
|
3703
|
-
snippet: truncateSnippet(win.raw)
|
|
3704
|
-
});
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
3630
|
for (const tag of tags) {
|
|
3708
3631
|
const selectors = tagSimpleSelectors(tag);
|
|
3709
3632
|
if (selectors.length === 0) continue;
|