@mehshekari/eslint-plugin-gsap-motion 3.2.0 → 3.3.0
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/README.md +5 -0
- package/lib/rules.mjs +321 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,6 +89,11 @@ so a finding waived that way still fails `gsap-motion audit`.
|
|
|
89
89
|
| `late-transform-origin` | warn | A `fromTo` whose origin is only in its to-vars while its from-vars scale, rotate or skew, which leaves an SVG element offset |
|
|
90
90
|
| `missing-reduced-motion` | warn | An animating file with no `prefers-reduced-motion` branch |
|
|
91
91
|
| `barrel-import` | warn | Importing from `gsap/all`, which pulls in every plugin |
|
|
92
|
+
| `tween-per-frame` | warn | A tween created every frame — in `onUpdate`, the ticker, an Observer callback, `useFrame` or a `requestAnimationFrame` loop |
|
|
93
|
+
| `paint-property` | warn | Animating `filter`, `backdropFilter` or `boxShadow`, which repaints the element every frame |
|
|
94
|
+
| `ungated-hover` | warn | A hover animation with no `(hover: hover)` gate, which a tap starts and nothing ends |
|
|
95
|
+
| `delay-chain` | warn | Three or more tweens in one scope sequenced by `delay`, which is a timeline nobody can retime |
|
|
96
|
+
| `unowned-loop` | info | An infinite repeat that nothing pauses, which keeps the ticker busy off screen |
|
|
92
97
|
| `not-parsed` | warn | A file that uses GSAP and that the audit's parser could not read, so no other rule checked it |
|
|
93
98
|
|
|
94
99
|
`not-parsed` is an info finding in the command line; ESLint has no info level.
|
package/lib/rules.mjs
CHANGED
|
@@ -53,6 +53,16 @@ const HINT = {
|
|
|
53
53
|
"Inside a context these register themselves and are reverted with it. Out here nothing is watching: move it into the useGSAP body, or keep the instance and revert/kill it from the cleanup.",
|
|
54
54
|
unrevertedContext:
|
|
55
55
|
"Keep it under a name and revert it when the component goes: `onUnmounted` in Vue, the function returned from `onMount` or `$effect` in Svelte, the `useEffect` cleanup in React, `astro:before-swap` in Astro. Everything created inside the context goes with it.",
|
|
56
|
+
tweenPerFrame:
|
|
57
|
+
"A tween per frame allocates an object per frame, and each one fights the last. Create a `gsap.quickTo()` or `quickSetter()` once outside the callback and call it inside — or, for scroll, scrub a timeline instead of writing values by hand.",
|
|
58
|
+
paintProperty:
|
|
59
|
+
"Filters and shadows repaint the layer every frame, and the cost grows with the painted area. Animate `opacity` on a pre-blurred or pre-shadowed copy, or move a transform instead, and measure before defending anything else.",
|
|
60
|
+
unownedLoop:
|
|
61
|
+
"An infinite repeat keeps the ticker busy while it is off screen. Pause it from an IntersectionObserver, or let a ScrollTrigger's `onToggle` own it, so it costs nothing when nobody can see it.",
|
|
62
|
+
ungatedHover:
|
|
63
|
+
"A tap fires `mouseenter` on a touch device and nothing fires the leave, so the effect sticks. Gate it with `gsap.matchMedia()` on `(hover: hover)`, and decide what a coarse pointer gets instead.",
|
|
64
|
+
delayChain:
|
|
65
|
+
"Delays are a choreography nobody can change: moving one beat means re-adding every number after it. Build a timeline and position each beat with a label or a relative offset — `\"<\"`, `\"-=0.2\"`.",
|
|
56
66
|
danglingListener:
|
|
57
67
|
"A listener that survives unmount keeps the component's closure — and its DOM nodes — alive. Name the handler and remove it in the cleanup, or pass `{ signal }` from an AbortController and abort it there.",
|
|
58
68
|
tweenPerEvent:
|
|
@@ -87,6 +97,10 @@ const HINT = {
|
|
|
87
97
|
/** Messages that do not vary with what was found. */
|
|
88
98
|
const MESSAGE = {
|
|
89
99
|
easedLoop: "An infinite repeat with an ease other than `none`.",
|
|
100
|
+
tweenPerFrame: "A tween is created here on every frame.",
|
|
101
|
+
unownedLoop: "An infinite repeat that nothing pauses.",
|
|
102
|
+
ungatedHover: "A hover animation with no `(hover: hover)` gate.",
|
|
103
|
+
delayChain: "Three or more tweens in this scope are sequenced by `delay`.",
|
|
90
104
|
easedScrub: "Easing inside a scrubbed ScrollTrigger timeline.",
|
|
91
105
|
neverCompletesChild:
|
|
92
106
|
"An endlessly repeating child in a timeline with an `onComplete`, which therefore never fires.",
|
|
@@ -414,6 +428,13 @@ function notPerEvent(ast, handler, node) {
|
|
|
414
428
|
}
|
|
415
429
|
|
|
416
430
|
/** Listener targets that outlive every component: the window, the document and its root elements. */
|
|
431
|
+
/**
|
|
432
|
+
* Events that fire once and are never removed by anyone: the document's own
|
|
433
|
+
* start-up. The corpus reported two `DOMContentLoaded` handlers in an Astro
|
|
434
|
+
* page as leaks, which is not what a leak is.
|
|
435
|
+
*/
|
|
436
|
+
const ONE_SHOT_EVENTS = new Set(["DOMContentLoaded", "load", "pageshow"]);
|
|
437
|
+
|
|
417
438
|
const GLOBAL_TARGETS = new Set([
|
|
418
439
|
"window",
|
|
419
440
|
"document",
|
|
@@ -547,6 +568,119 @@ function mountHook(ast, node) {
|
|
|
547
568
|
return null;
|
|
548
569
|
}
|
|
549
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Properties whose animation repaints the element rather than moving a layer
|
|
573
|
+
* the compositor already has.
|
|
574
|
+
*
|
|
575
|
+
* `clipPath` is deliberately absent. An inset or circle reveal is the technique
|
|
576
|
+
* this skill recommends for image reveals, it is cheap on a simple shape, and a
|
|
577
|
+
* rule that fires on its own guidance is noise.
|
|
578
|
+
*/
|
|
579
|
+
const PAINT_PROPERTIES = new Set([
|
|
580
|
+
"filter",
|
|
581
|
+
"webkitFilter",
|
|
582
|
+
"backdropFilter",
|
|
583
|
+
"boxShadow",
|
|
584
|
+
]);
|
|
585
|
+
|
|
586
|
+
const HOVER_EVENTS = new Set([
|
|
587
|
+
"pointerenter",
|
|
588
|
+
"pointerover",
|
|
589
|
+
"mouseenter",
|
|
590
|
+
"mouseover",
|
|
591
|
+
]);
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Every function that runs once per frame: the ticker, R3F's `useFrame`, an
|
|
595
|
+
* `onUpdate`/`onMove`/`onChange` callback, and a function that schedules
|
|
596
|
+
* itself with `requestAnimationFrame`.
|
|
597
|
+
*/
|
|
598
|
+
function perFrameScopes(ast) {
|
|
599
|
+
const scopes = [];
|
|
600
|
+
const add = (node) => {
|
|
601
|
+
const fn = node && unwrap(node);
|
|
602
|
+
if (isFunction(fn)) scopes.push(fn);
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
for (const call of findAll(ast, (node) => {
|
|
606
|
+
const name = calleeName(node);
|
|
607
|
+
return name === "gsap.ticker.add" || name === "useFrame";
|
|
608
|
+
})) {
|
|
609
|
+
add(call.arguments[0]);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* `onUpdate`, `onMove` and `onChange` are per-frame in a tween, a
|
|
614
|
+
* ScrollTrigger, an Observer or a Draggable — and nowhere else. Every control
|
|
615
|
+
* and form library in the world has an `onChange`, and the corpus duly
|
|
616
|
+
* reported a Three.js `traverse` inside one of them as a per-frame tween.
|
|
617
|
+
*/
|
|
618
|
+
const PER_FRAME_KEYS = new Set(["onUpdate", "onMove", "onChange"]);
|
|
619
|
+
const GSAP_CONFIG = /^(?:gsap\.|ScrollTrigger\.|Observer\.|Draggable\.|ScrollSmoother\.|Flip\.)/;
|
|
620
|
+
|
|
621
|
+
for (const property of findAll(
|
|
622
|
+
ast,
|
|
623
|
+
(node) => node.type === "Property" && PER_FRAME_KEYS.has(keyName(node)),
|
|
624
|
+
)) {
|
|
625
|
+
const owner = ancestorsOf(ast, property).find(
|
|
626
|
+
(node) => node.type === "CallExpression",
|
|
627
|
+
);
|
|
628
|
+
if (!owner) continue;
|
|
629
|
+
const name = calleeName(owner);
|
|
630
|
+
if (tweenMethod(owner) === null && !(name && GSAP_CONFIG.test(name))) continue;
|
|
631
|
+
add(property.value);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
for (const call of findAll(
|
|
635
|
+
ast,
|
|
636
|
+
(node) => calleeName(node) === "requestAnimationFrame",
|
|
637
|
+
)) {
|
|
638
|
+
const fn = resolveFunction(ast, call.arguments[0]);
|
|
639
|
+
if (isFunction(fn) && contains(fn, call)) scopes.push(fn);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return scopes;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** The handlers a hover starts: a listener's callback, or a JSX `on*` prop. */
|
|
646
|
+
function hoverHandlers(ast) {
|
|
647
|
+
const handlers = [];
|
|
648
|
+
|
|
649
|
+
for (const call of findAll(
|
|
650
|
+
ast,
|
|
651
|
+
(node) => listenerMethod(node) === "addEventListener",
|
|
652
|
+
)) {
|
|
653
|
+
const event = staticString(call.arguments[0]);
|
|
654
|
+
if (event === null || !HOVER_EVENTS.has(event)) continue;
|
|
655
|
+
const fn = resolveFunction(ast, call.arguments[1]);
|
|
656
|
+
if (isFunction(fn)) handlers.push(fn);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
for (const attribute of findAll(
|
|
660
|
+
ast,
|
|
661
|
+
(node) => node.type === "JSXAttribute" && node.name.type === "JSXIdentifier",
|
|
662
|
+
)) {
|
|
663
|
+
if (!/^on(?:Mouse|Pointer)(?:Enter|Over)$/.test(attribute.name.name)) continue;
|
|
664
|
+
if (attribute.value?.type !== "JSXExpressionContainer") continue;
|
|
665
|
+
const fn = resolveFunction(ast, attribute.value.expression);
|
|
666
|
+
if (isFunction(fn)) handlers.push(fn);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return handlers;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Whether anything in the file pauses, kills or reverts what is kept as `name`. */
|
|
673
|
+
function stopped(ast, name) {
|
|
674
|
+
if (!name) return false;
|
|
675
|
+
return (
|
|
676
|
+
findAll(ast, (node) => {
|
|
677
|
+
const method = methodName(node);
|
|
678
|
+
if (method !== "pause" && !TEARDOWN.has(method)) return false;
|
|
679
|
+
return dottedName(unwrap(node.callee).object) === name;
|
|
680
|
+
}).length > 0
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
550
684
|
export const RULES = [
|
|
551
685
|
// --- Lifecycle ------------------------------------------------------------
|
|
552
686
|
{
|
|
@@ -687,6 +821,7 @@ export const RULES = [
|
|
|
687
821
|
const event = staticString(call.arguments[0]);
|
|
688
822
|
const handler = unwrap(call.arguments[1]);
|
|
689
823
|
if (event === null || !/^[\w:-]+$/.test(event) || !handler) return [];
|
|
824
|
+
if (ONE_SHOT_EVENTS.has(event)) return [];
|
|
690
825
|
const options = unwrap(call.arguments[2]);
|
|
691
826
|
const once = unwrap(propertyOf(options, "once")?.value);
|
|
692
827
|
const callee = unwrap(call.callee);
|
|
@@ -1184,6 +1319,177 @@ export const RULES = [
|
|
|
1184
1319
|
return findings;
|
|
1185
1320
|
},
|
|
1186
1321
|
},
|
|
1322
|
+
{
|
|
1323
|
+
id: "tween-per-frame",
|
|
1324
|
+
level: "warn",
|
|
1325
|
+
description:
|
|
1326
|
+
"A tween created every frame — in `onUpdate`, the ticker, an Observer callback, `useFrame` or a `requestAnimationFrame` loop",
|
|
1327
|
+
/**
|
|
1328
|
+
* `gsap.set` is left out: writing a value straight to an element is what a
|
|
1329
|
+
* per-frame callback is for, and `quickSetter` is the faster shape of the
|
|
1330
|
+
* same thing rather than a different one. What this catches is a tween —
|
|
1331
|
+
* an object with its own duration and ease — being built sixty times a
|
|
1332
|
+
* second, each one overwriting the last before it can finish.
|
|
1333
|
+
*/
|
|
1334
|
+
test(file) {
|
|
1335
|
+
const scopes = perFrameScopes(file.ast);
|
|
1336
|
+
if (scopes.length === 0) return [];
|
|
1337
|
+
|
|
1338
|
+
const tweens = findAll(file.ast, (node) => {
|
|
1339
|
+
const method = tweenMethod(node);
|
|
1340
|
+
return method !== null && method !== "set";
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
const reported = new Set();
|
|
1344
|
+
const findings = [];
|
|
1345
|
+
for (const scope of scopes) {
|
|
1346
|
+
for (const tween of tweens) {
|
|
1347
|
+
if (!contains(scope, tween) || reported.has(tween.start)) continue;
|
|
1348
|
+
reported.add(tween.start);
|
|
1349
|
+
findings.push({
|
|
1350
|
+
index: tween.start,
|
|
1351
|
+
message: MESSAGE.tweenPerFrame,
|
|
1352
|
+
hint: HINT.tweenPerFrame,
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return findings;
|
|
1357
|
+
},
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
id: "paint-property",
|
|
1361
|
+
level: "warn",
|
|
1362
|
+
description:
|
|
1363
|
+
"Animating `filter`, `backdropFilter` or `boxShadow`, which repaints the element every frame",
|
|
1364
|
+
/** Read exactly as layout-property reads its own keys, and `set` is skipped too. */
|
|
1365
|
+
test(file) {
|
|
1366
|
+
const animates = (node) => {
|
|
1367
|
+
const method = tweenMethod(node);
|
|
1368
|
+
return method !== null && method !== "set";
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
return findAll(file.ast, animates).flatMap((call) => {
|
|
1372
|
+
const seen = new Set();
|
|
1373
|
+
return varsObjects(call, tweenMethod(call))
|
|
1374
|
+
.flatMap((vars) => vars.properties)
|
|
1375
|
+
.filter((property) => {
|
|
1376
|
+
const key = keyName(property);
|
|
1377
|
+
if (!PAINT_PROPERTIES.has(key) || seen.has(key)) return false;
|
|
1378
|
+
seen.add(key);
|
|
1379
|
+
return true;
|
|
1380
|
+
})
|
|
1381
|
+
.map((property) => ({
|
|
1382
|
+
index: property.start,
|
|
1383
|
+
message: `Animating \`${keyName(property)}\` repaints on every frame.`,
|
|
1384
|
+
hint: HINT.paintProperty,
|
|
1385
|
+
}));
|
|
1386
|
+
});
|
|
1387
|
+
},
|
|
1388
|
+
},
|
|
1389
|
+
{
|
|
1390
|
+
id: "unowned-loop",
|
|
1391
|
+
level: "info",
|
|
1392
|
+
description:
|
|
1393
|
+
"An infinite repeat that nothing pauses, which keeps the ticker busy off screen",
|
|
1394
|
+
/**
|
|
1395
|
+
* Info, because "nothing pauses it" is read from one file and a loop can be
|
|
1396
|
+
* owned from elsewhere. A loop is taken as owned when its name is paused,
|
|
1397
|
+
* killed or reverted anywhere here, when the file builds an
|
|
1398
|
+
* IntersectionObserver, when the tween carries a `scrollTrigger` that
|
|
1399
|
+
* toggles it, or when a context holds it — unmount reverts that one.
|
|
1400
|
+
*
|
|
1401
|
+
* The last of those came from a real site's loader, where three loops sit
|
|
1402
|
+
* in a `useGSAP` body inside `matchMedia`. Reporting them would have meant
|
|
1403
|
+
* "nothing stops this" about an animation the component's own unmount stops.
|
|
1404
|
+
* What is left — a loop running while it is off screen but still mounted —
|
|
1405
|
+
* is a measurement, and 3.4's `inspect` is where that belongs.
|
|
1406
|
+
*/
|
|
1407
|
+
test(file) {
|
|
1408
|
+
const observed = findAll(
|
|
1409
|
+
file.ast,
|
|
1410
|
+
(node) =>
|
|
1411
|
+
node.type === "NewExpression" &&
|
|
1412
|
+
dottedName(node.callee) === "IntersectionObserver",
|
|
1413
|
+
).length > 0;
|
|
1414
|
+
if (observed) return [];
|
|
1415
|
+
|
|
1416
|
+
const inside = gsapContexts(file.ast);
|
|
1417
|
+
|
|
1418
|
+
return findAll(file.ast, (node) => tweenMethod(node) !== null).flatMap((call) => {
|
|
1419
|
+
if (inside(call)) return [];
|
|
1420
|
+
const vars = ownVars(call, tweenMethod(call));
|
|
1421
|
+
if (!vars) return [];
|
|
1422
|
+
const repeat = propertyOf(vars, "repeat");
|
|
1423
|
+
if (!repeat || numberValue(repeat.value) !== -1) return [];
|
|
1424
|
+
if (propertyOf(vars, "scrollTrigger")) return [];
|
|
1425
|
+
if (stopped(file.ast, keptUnder(file.ast, call)?.name)) return [];
|
|
1426
|
+
return [
|
|
1427
|
+
{
|
|
1428
|
+
index: repeat.start,
|
|
1429
|
+
message: MESSAGE.unownedLoop,
|
|
1430
|
+
hint: HINT.unownedLoop,
|
|
1431
|
+
},
|
|
1432
|
+
];
|
|
1433
|
+
});
|
|
1434
|
+
},
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
id: "ungated-hover",
|
|
1438
|
+
level: "warn",
|
|
1439
|
+
description:
|
|
1440
|
+
"A hover animation with no `(hover: hover)` gate, which a tap starts and nothing ends",
|
|
1441
|
+
/**
|
|
1442
|
+
* The gate is looked for as text, anywhere in the file: a `matchMedia`
|
|
1443
|
+
* condition, a CSS string, a constant. One finding per handler, on its first
|
|
1444
|
+
* tween, because the handler is what needs the gate.
|
|
1445
|
+
*/
|
|
1446
|
+
test(file) {
|
|
1447
|
+
if (/\(\s*(?:any-)?hover\s*:\s*hover\s*\)/.test(file.code)) return [];
|
|
1448
|
+
|
|
1449
|
+
return hoverHandlers(file.ast).flatMap((handler) => {
|
|
1450
|
+
const [tween] = findAll(file.ast, (node) => tweenMethod(node) !== null).filter(
|
|
1451
|
+
(node) => contains(handler, node),
|
|
1452
|
+
);
|
|
1453
|
+
if (!tween) return [];
|
|
1454
|
+
return [
|
|
1455
|
+
{ index: tween.start, message: MESSAGE.ungatedHover, hint: HINT.ungatedHover },
|
|
1456
|
+
];
|
|
1457
|
+
});
|
|
1458
|
+
},
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
id: "delay-chain",
|
|
1462
|
+
level: "warn",
|
|
1463
|
+
description:
|
|
1464
|
+
"Three or more tweens in one scope sequenced by `delay`, which is a timeline nobody can retime",
|
|
1465
|
+
/**
|
|
1466
|
+
* Three is the threshold because two tweens with delays are a pair, and a
|
|
1467
|
+
* pair is still readable. Counted per scope — the function they share — so
|
|
1468
|
+
* three components each with one delayed tween are not a chain. A tween on a
|
|
1469
|
+
* timeline is not counted: a timeline already has positions.
|
|
1470
|
+
*/
|
|
1471
|
+
test(file) {
|
|
1472
|
+
const scopes = new Map();
|
|
1473
|
+
|
|
1474
|
+
for (const call of findAll(file.ast, (node) => tweenMethod(node) !== null)) {
|
|
1475
|
+
if (chainStart(call) !== call) continue;
|
|
1476
|
+
const vars = ownVars(call, tweenMethod(call));
|
|
1477
|
+
const delay = vars && propertyOf(vars, "delay");
|
|
1478
|
+
if (!delay || !(numberValue(delay.value) > 0)) continue;
|
|
1479
|
+
const scope = scopeOf(file.ast, call) ?? file.ast;
|
|
1480
|
+
if (!scopes.has(scope)) scopes.set(scope, []);
|
|
1481
|
+
scopes.get(scope).push(call);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
return [...scopes.values()]
|
|
1485
|
+
.filter((calls) => calls.length >= 3)
|
|
1486
|
+
.map((calls) => ({
|
|
1487
|
+
index: calls[0].start,
|
|
1488
|
+
message: MESSAGE.delayChain,
|
|
1489
|
+
hint: HINT.delayChain,
|
|
1490
|
+
}));
|
|
1491
|
+
},
|
|
1492
|
+
},
|
|
1187
1493
|
{
|
|
1188
1494
|
id: "missing-reduced-motion",
|
|
1189
1495
|
level: "warn",
|
|
@@ -1191,23 +1497,28 @@ export const RULES = [
|
|
|
1191
1497
|
"An animating file with no `prefers-reduced-motion` branch",
|
|
1192
1498
|
/**
|
|
1193
1499
|
* A branch is the query written in code — a string, a template or JSX
|
|
1194
|
-
* text. A comment that mentions it is not a branch.
|
|
1195
|
-
*
|
|
1500
|
+
* text. A comment that mentions it is not a branch.
|
|
1501
|
+
*
|
|
1502
|
+
* Only a file that builds an animation of its own is reported, and the
|
|
1503
|
+
* finding lands on the first one. Three things are not that:
|
|
1504
|
+
* `registerPlugin` and `killTweensOf`, which animate nothing; `set`, which
|
|
1505
|
+
* has no duration; and a method called on a timeline the file was handed,
|
|
1506
|
+
* because a helper that adds beats to its caller's timeline does not own
|
|
1507
|
+
* the reduced-motion decision — the caller does.
|
|
1196
1508
|
*/
|
|
1197
1509
|
test(file) {
|
|
1198
1510
|
if (!file.usesGsap) return [];
|
|
1199
1511
|
if (findAll(file.ast, (node) => spells(node, "prefers-reduced-motion")).length) {
|
|
1200
1512
|
return [];
|
|
1201
1513
|
}
|
|
1202
|
-
if (!findAll(file.ast, (node) => isGsapCall(node)).length) return [];
|
|
1203
1514
|
|
|
1204
|
-
const first = findAll(
|
|
1205
|
-
|
|
1206
|
-
(
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1515
|
+
const [first] = findAll(file.ast, (node) => {
|
|
1516
|
+
const method = tweenMethod(node);
|
|
1517
|
+
if (method === null || method === "set") return false;
|
|
1518
|
+
/** Built here, rather than added to something passed in. */
|
|
1519
|
+
return calleeName(node)?.startsWith("gsap.") === true;
|
|
1520
|
+
}).sort((a, b) => a.start - b.start);
|
|
1521
|
+
if (!first) return [];
|
|
1211
1522
|
|
|
1212
1523
|
return [
|
|
1213
1524
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mehshekari/eslint-plugin-gsap-motion",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "The gsap-motion audit as ESLint rules: GSAP tweens that outlive their component, per-event allocations and missing reduced-motion branches, reported in your editor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|