@mehshekari/gsap-motion 4.2.0 → 4.2.1
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 +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
- package/skill/scripts/lib/ast.mjs +37 -6
- package/skill/scripts/lib/rules.mjs +67 -27
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ The audit needs no install. Pin the version, so a new rule cannot fail your
|
|
|
32
32
|
build without warning:
|
|
33
33
|
|
|
34
34
|
```json
|
|
35
|
-
"lint": "eslint && npx @mehshekari/gsap-motion@4.2.
|
|
35
|
+
"lint": "eslint && npx @mehshekari/gsap-motion@4.2.1 audit src --quiet"
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Or install it with `npm install --save-dev --save-exact @mehshekari/gsap-motion`.
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Senior motion engineer for GSAP. Turns a visual or interaction idea
|
|
|
4
4
|
license: MIT. See LICENSE; GSAP itself is licensed separately, see NOTICE.md
|
|
5
5
|
compatibility: Any Agent Skills client; written for Claude Code. GSAP 3.13+, where every plugin is free. React guidance targets @gsap/react 2.x, React 18/19 and the Next.js App Router. The audit scripts need Node.js 22+ and nothing else.
|
|
6
6
|
metadata:
|
|
7
|
-
version: "4.2.
|
|
7
|
+
version: "4.2.1"
|
|
8
8
|
verified-gsap: "3.15"
|
|
9
9
|
verified-gsap-react: "2.1"
|
|
10
10
|
---
|
|
@@ -478,12 +478,43 @@ export function gsapContexts(root) {
|
|
|
478
478
|
}
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
481
|
+
/**
|
|
482
|
+
* A matchMedia is a context too: `mm.revert()` reverts what its `add`
|
|
483
|
+
* callbacks made. Its callback is recognised on `gsap.matchMedia().add` and on
|
|
484
|
+
* any name the file assigns `gsap.matchMedia()` to, followed when it is handed
|
|
485
|
+
* over by name. Whether the matchMedia itself is ever reverted is
|
|
486
|
+
* `unmanaged-instance`'s to report, not a reason to distrust its callbacks.
|
|
487
|
+
*/
|
|
488
|
+
const matchMedias = new Set(
|
|
489
|
+
findAll(
|
|
490
|
+
root,
|
|
491
|
+
(node) =>
|
|
492
|
+
(node.type === "VariableDeclarator" &&
|
|
493
|
+
Boolean(node.init) &&
|
|
494
|
+
calleeName(unwrap(node.init)) === "gsap.matchMedia") ||
|
|
495
|
+
(node.type === "AssignmentExpression" &&
|
|
496
|
+
calleeName(unwrap(node.right)) === "gsap.matchMedia"),
|
|
497
|
+
)
|
|
498
|
+
.map((node) => dottedName(node.type === "VariableDeclarator" ? node.id : node.left))
|
|
499
|
+
.filter(Boolean),
|
|
500
|
+
);
|
|
501
|
+
const isMatchMediaAdd = (node) => {
|
|
502
|
+
if (methodName(node) !== "add") return false;
|
|
503
|
+
const object = unwrap(unwrap(node.callee).object);
|
|
504
|
+
return calleeName(object) === "gsap.matchMedia" || matchMedias.has(dottedName(object));
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const scopes = [
|
|
508
|
+
...findAll(
|
|
509
|
+
root,
|
|
510
|
+
(node) =>
|
|
511
|
+
node.type === "CallExpression" &&
|
|
512
|
+
(isHook(node) || calleeName(node) === "gsap.context" || safe.has(bareName(node))),
|
|
513
|
+
).flatMap((call) => call.arguments),
|
|
514
|
+
...findAll(root, isMatchMediaAdd).flatMap((call) =>
|
|
515
|
+
call.arguments.map((argument) => resolveFunction(root, argument)).filter(Boolean),
|
|
516
|
+
),
|
|
517
|
+
];
|
|
487
518
|
|
|
488
519
|
const counts = new Map();
|
|
489
520
|
const functions = new Map();
|
|
@@ -1606,38 +1606,47 @@ export const RULES = [
|
|
|
1606
1606
|
id: "matchmedia-never-runs",
|
|
1607
1607
|
level: "warn",
|
|
1608
1608
|
description:
|
|
1609
|
-
"A matchMedia callback that branches on
|
|
1609
|
+
"A matchMedia callback that branches on reduced motion, when every condition needs it",
|
|
1610
1610
|
/**
|
|
1611
1611
|
* `mm.add({ reduced: "(prefers-reduced-motion: reduce)" }, (context) => …)`
|
|
1612
|
-
* whose callback
|
|
1612
|
+
* whose callback branches on the preference. The callback runs only while a
|
|
1613
1613
|
* named condition matches, and a visitor with no preference matches none —
|
|
1614
1614
|
* so the branch written for them never runs, and neither does the
|
|
1615
1615
|
* animation in it. Found in a head-to-head test on a real hero, where the
|
|
1616
|
-
* whole page shipped without motion.
|
|
1616
|
+
* whole page shipped without motion, and again in phase 8's first round.
|
|
1617
1617
|
*
|
|
1618
|
-
*
|
|
1619
|
-
*
|
|
1620
|
-
*
|
|
1618
|
+
* Branching is the whole signal: reading `conditions`, or asking
|
|
1619
|
+
* `window.matchMedia` about reduced motion inside the callback. A
|
|
1620
|
+
* reduced-only `add` that does not branch is GSAP's documented pattern for
|
|
1621
|
+
* setting end states, meant to run for those visitors only, and is correct.
|
|
1622
|
+
*
|
|
1623
|
+
* The conditions are read when they are written in the call, or kept in a
|
|
1624
|
+
* `const` the file declares once. An imported object, or a `let` that may be
|
|
1625
|
+
* reassigned, cannot be judged here and is left alone.
|
|
1621
1626
|
*/
|
|
1622
1627
|
test(file) {
|
|
1623
1628
|
const REDUCE = /prefers-reduced-motion\s*:\s*reduce/i;
|
|
1629
|
+
const PREFERENCE = /prefers-reduced-motion/i;
|
|
1624
1630
|
|
|
1625
|
-
/** Names bound to `gsap.matchMedia()` in this file. */
|
|
1631
|
+
/** Names bound to `gsap.matchMedia()` in this file, at declaration or later. */
|
|
1626
1632
|
const media = new Set(
|
|
1627
1633
|
findAll(
|
|
1628
1634
|
file.ast,
|
|
1629
1635
|
(node) =>
|
|
1630
|
-
node.type === "VariableDeclarator" &&
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1636
|
+
(node.type === "VariableDeclarator" &&
|
|
1637
|
+
node.id.type === "Identifier" &&
|
|
1638
|
+
/**
|
|
1639
|
+
* `let tl;` has no initializer, and `calleeName` does not accept
|
|
1640
|
+
* null. Every fixture initialised its declarations, so 133 passing
|
|
1641
|
+
* tests missed this; the first real project threw on it and took
|
|
1642
|
+
* the whole audit down with it.
|
|
1643
|
+
*/
|
|
1644
|
+
Boolean(node.init) &&
|
|
1645
|
+
calleeName(unwrap(node.init)) === "gsap.matchMedia") ||
|
|
1646
|
+
(node.type === "AssignmentExpression" &&
|
|
1647
|
+
node.left.type === "Identifier" &&
|
|
1648
|
+
calleeName(unwrap(node.right)) === "gsap.matchMedia"),
|
|
1649
|
+
).map((node) => (node.type === "VariableDeclarator" ? node.id.name : node.left.name)),
|
|
1641
1650
|
);
|
|
1642
1651
|
|
|
1643
1652
|
const onMatchMedia = (call) => {
|
|
@@ -1648,8 +1657,45 @@ export const RULES = [
|
|
|
1648
1657
|
return calleeName(object) === "gsap.matchMedia";
|
|
1649
1658
|
};
|
|
1650
1659
|
|
|
1651
|
-
/**
|
|
1660
|
+
/** The conditions as written: in the call, or in a `const` the file declares once. */
|
|
1661
|
+
const conditionsOf = (argument) => {
|
|
1662
|
+
const target = unwrap(argument);
|
|
1663
|
+
if (target?.type !== "Identifier") return target;
|
|
1664
|
+
const declarators = findAll(
|
|
1665
|
+
file.ast,
|
|
1666
|
+
(node) =>
|
|
1667
|
+
node.type === "VariableDeclarator" &&
|
|
1668
|
+
node.id.type === "Identifier" &&
|
|
1669
|
+
node.id.name === target.name,
|
|
1670
|
+
);
|
|
1671
|
+
if (declarators.length !== 1) return null;
|
|
1672
|
+
const [declarator] = declarators;
|
|
1673
|
+
return parentOf(file.ast, declarator)?.kind === "const" ? unwrap(declarator.init) : null;
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
/** Every condition needs reduced motion: one query string, or an object of them. */
|
|
1677
|
+
const everyReduces = (conditions) => {
|
|
1678
|
+
const query = staticString(conditions);
|
|
1679
|
+
if (typeof query === "string") return REDUCE.test(query);
|
|
1680
|
+
if (conditions?.type !== "ObjectExpression" || !conditions.properties.length) return false;
|
|
1681
|
+
return conditions.properties.every((property) => {
|
|
1682
|
+
const value = staticString(property.value);
|
|
1683
|
+
return typeof value === "string" && REDUCE.test(value);
|
|
1684
|
+
});
|
|
1685
|
+
};
|
|
1686
|
+
|
|
1687
|
+
/** Does the callback read its conditions, or ask the browser about reduced motion? */
|
|
1652
1688
|
const branches = (fn) => {
|
|
1689
|
+
const asksTheBrowser =
|
|
1690
|
+
findAll(fn.body, (node) => {
|
|
1691
|
+
if (node.type !== "CallExpression") return false;
|
|
1692
|
+
const name = calleeName(node);
|
|
1693
|
+
if (name !== "window.matchMedia" && name !== "matchMedia") return false;
|
|
1694
|
+
const query = staticString(node.arguments[0]);
|
|
1695
|
+
return typeof query === "string" && PREFERENCE.test(query);
|
|
1696
|
+
}).length > 0;
|
|
1697
|
+
if (asksTheBrowser) return true;
|
|
1698
|
+
|
|
1653
1699
|
const [param] = fn.params;
|
|
1654
1700
|
if (!param) return false;
|
|
1655
1701
|
if (param.type === "ObjectPattern") {
|
|
@@ -1670,20 +1716,14 @@ export const RULES = [
|
|
|
1670
1716
|
|
|
1671
1717
|
return findAll(file.ast, (node) => methodName(node) === "add" && onMatchMedia(node))
|
|
1672
1718
|
.filter((call) => {
|
|
1673
|
-
|
|
1674
|
-
if (conditions?.type !== "ObjectExpression" || !conditions.properties.length) return false;
|
|
1675
|
-
const everyReduces = conditions.properties.every((property) => {
|
|
1676
|
-
const value = staticString(property.value);
|
|
1677
|
-
return typeof value === "string" && REDUCE.test(value);
|
|
1678
|
-
});
|
|
1679
|
-
if (!everyReduces) return false;
|
|
1719
|
+
if (!everyReduces(conditionsOf(call.arguments[0]))) return false;
|
|
1680
1720
|
const callback = resolveFunction(file.ast, call.arguments[1]);
|
|
1681
1721
|
return Boolean(callback) && branches(callback);
|
|
1682
1722
|
})
|
|
1683
1723
|
.map((call) => ({
|
|
1684
1724
|
index: call.start,
|
|
1685
1725
|
message:
|
|
1686
|
-
"This matchMedia callback branches on
|
|
1726
|
+
"This matchMedia callback branches on reduced motion, but every condition needs reduced motion — so it never runs for a visitor without that preference.",
|
|
1687
1727
|
hint: HINT.matchMediaNeverRuns,
|
|
1688
1728
|
}));
|
|
1689
1729
|
},
|