@mehshekari/gsap-motion 4.6.0 → 4.6.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/audit-gsap.mjs +84 -8
- package/skill/scripts/lib/rules.mjs +5 -1
- package/skill/scripts/lib/source.mjs +8 -1
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.6.
|
|
35
|
+
"lint": "eslint && npx @mehshekari/gsap-motion@4.6.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.6.
|
|
7
|
+
version: "4.6.1"
|
|
8
8
|
verified-gsap: "3.15"
|
|
9
9
|
verified-gsap-react: "2.1"
|
|
10
10
|
---
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
*
|
|
33
33
|
* Exit code is 1 when any error-level finding is reported, 0 otherwise.
|
|
34
34
|
*/
|
|
35
|
-
import { existsSync, statSync } from "node:fs";
|
|
36
|
-
import { dirname, resolve } from "node:path";
|
|
35
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
36
|
+
import { dirname, join, resolve } from "node:path";
|
|
37
37
|
|
|
38
38
|
import { check, mentionsGsap, notParsed } from "./lib/audit.mjs";
|
|
39
39
|
import { pluginRegistrations, RULES } from "./lib/rules.mjs";
|
|
@@ -62,6 +62,54 @@ for (const path of paths) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* `compilerOptions.paths` from the project's tsconfig.json, as
|
|
67
|
+
* `[prefix, [target prefixes]]`: `"@/*": ["./src/*"]` becomes
|
|
68
|
+
* `["@/", ["./src/"]]`, against `baseUrl` when one is set. A tsconfig is JSON
|
|
69
|
+
* with comments and trailing commas, so those are stripped first; one that
|
|
70
|
+
* still does not parse gives no aliases rather than an error.
|
|
71
|
+
*/
|
|
72
|
+
function tsconfigPaths(root) {
|
|
73
|
+
try {
|
|
74
|
+
const text = withoutComments(readFileSync(join(root, "tsconfig.json"), "utf8")).replace(/,(\s*[}\]])/g, "$1");
|
|
75
|
+
const { compilerOptions = {} } = JSON.parse(text);
|
|
76
|
+
const base = compilerOptions.baseUrl ?? ".";
|
|
77
|
+
return Object.entries(compilerOptions.paths ?? {}).map(([key, targets]) => [
|
|
78
|
+
key.replace(/\*$/, ""),
|
|
79
|
+
targets.map((target) => join(base, target.replace(/\*$/, ""))),
|
|
80
|
+
]);
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* JSON with its comments removed, outside strings only. A regex over the whole
|
|
88
|
+
* text took the `/*` in `"**\/*.ts"` — in every Next tsconfig's `include` — for
|
|
89
|
+
* the start of a comment, broke the JSON, and lost every alias.
|
|
90
|
+
*/
|
|
91
|
+
function withoutComments(text) {
|
|
92
|
+
let out = "";
|
|
93
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
94
|
+
const char = text[i];
|
|
95
|
+
if (char === '"') {
|
|
96
|
+
const end = text.slice(i + 1).search(/(?<!\\)"/);
|
|
97
|
+
const close = end === -1 ? text.length : i + 1 + end;
|
|
98
|
+
out += text.slice(i, close + 1);
|
|
99
|
+
i = close;
|
|
100
|
+
} else if (char === "/" && text[i + 1] === "/") {
|
|
101
|
+
while (i < text.length && text[i] !== "\n") i += 1;
|
|
102
|
+
out += "\n";
|
|
103
|
+
} else if (char === "/" && text[i + 1] === "*") {
|
|
104
|
+
const end = text.indexOf("*/", i + 2);
|
|
105
|
+
i = end === -1 ? text.length : end + 1;
|
|
106
|
+
} else {
|
|
107
|
+
out += char;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
65
113
|
// --- Run ---------------------------------------------------------------------
|
|
66
114
|
|
|
67
115
|
const findings = [];
|
|
@@ -115,17 +163,45 @@ const registered = new Set(sources.flatMap((source) => [...pluginRegistrations(s
|
|
|
115
163
|
|
|
116
164
|
/**
|
|
117
165
|
* Who imports each file, among everything read — a caller need not use GSAP
|
|
118
|
-
* itself. Relative imports
|
|
119
|
-
*
|
|
166
|
+
* itself. Relative imports, and aliases the project's tsconfig maps in
|
|
167
|
+
* `compilerOptions.paths` (`"@/*": ["./src/*"]`).
|
|
168
|
+
*
|
|
169
|
+
* An alias that cannot be resolved may still name the file, and a verdict that
|
|
170
|
+
* rests on some of a file's callers is a guess. A real project imported one
|
|
171
|
+
* helper relatively from one caller and through `@/` from another; only the
|
|
172
|
+
* first was seen. So when an unresolved alias ends in a file's name, that
|
|
173
|
+
* file's importers are marked unknown, and it is judged alone.
|
|
120
174
|
*/
|
|
121
175
|
const byPath = new Map(loaded.map((source) => [resolve(source.path), source]));
|
|
122
176
|
const EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", "/index.ts", "/index.tsx", "/index.js"];
|
|
177
|
+
const aliases = tsconfigPaths(ROOT);
|
|
178
|
+
const resolveSpecifier = (from, specifier) => {
|
|
179
|
+
const bases = specifier.startsWith(".")
|
|
180
|
+
? [resolve(dirname(from), specifier)]
|
|
181
|
+
: aliases.flatMap(([prefix, targets]) =>
|
|
182
|
+
specifier.startsWith(prefix) ? targets.map((target) => resolve(ROOT, target + specifier.slice(prefix.length))) : [],
|
|
183
|
+
);
|
|
184
|
+
for (const base of bases) {
|
|
185
|
+
const target = EXTENSIONS.map((ext) => byPath.get(base + ext)).find(Boolean);
|
|
186
|
+
if (target) return target;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
};
|
|
190
|
+
/** A bare package name — `gsap`, `react`, `@scope/pkg` — is not an alias. */
|
|
191
|
+
const looksLikeAlias = (specifier) => /^(?:@\/|~\/|#)/.test(specifier) || aliases.some(([prefix]) => specifier.startsWith(prefix));
|
|
192
|
+
const stem = (path) => path.replace(/\\/g, "/").split("/").at(-1).replace(/\.[^.]+$/, "");
|
|
193
|
+
|
|
123
194
|
for (const source of loaded) source.importers = [];
|
|
124
195
|
for (const source of loaded) {
|
|
125
|
-
for (const [, specifier] of source.code.matchAll(/\bimport\s+(?!type\b)[^;]*?\bfrom\s*["'](
|
|
126
|
-
|
|
127
|
-
const target =
|
|
128
|
-
if (target && target !== source)
|
|
196
|
+
for (const [, specifier] of source.code.matchAll(/\bimport\s+(?!type\b)[^;]*?\bfrom\s*["']([^"']+)["']/g)) {
|
|
197
|
+
if (!specifier.startsWith(".") && !looksLikeAlias(specifier)) continue;
|
|
198
|
+
const target = resolveSpecifier(source.path, specifier);
|
|
199
|
+
if (target && target !== source) {
|
|
200
|
+
target.importers.push(source);
|
|
201
|
+
} else if (!target && looksLikeAlias(specifier)) {
|
|
202
|
+
const name = specifier.split("/").at(-1);
|
|
203
|
+
for (const candidate of loaded) if (stem(candidate.path) === name) candidate.importersUnknown = true;
|
|
204
|
+
}
|
|
129
205
|
}
|
|
130
206
|
}
|
|
131
207
|
|
|
@@ -1895,7 +1895,11 @@ export const RULES = [
|
|
|
1895
1895
|
* returned early under reduced motion. Only the full audit knows the
|
|
1896
1896
|
* importers; the editor and the write hook see one file, and report it.
|
|
1897
1897
|
*/
|
|
1898
|
-
if (
|
|
1898
|
+
if (
|
|
1899
|
+
file.importers?.length &&
|
|
1900
|
+
!file.importersUnknown &&
|
|
1901
|
+
file.importers.every((importer) => !importer.parseError && hasReducedBranch(importer))
|
|
1902
|
+
) {
|
|
1899
1903
|
return [];
|
|
1900
1904
|
}
|
|
1901
1905
|
|
|
@@ -249,7 +249,14 @@ export function fromText(raw, path, display = path) {
|
|
|
249
249
|
*/
|
|
250
250
|
usesGsap:
|
|
251
251
|
/\bfrom ["']gsap(?:\/|["'])/.test(code) ||
|
|
252
|
-
/\bimport\s+(?!type\b)(?:gsap\b|\{[^}]*\bgsap\b[^}]*\})[^;]*?\bfrom\s*["']/.test(code)
|
|
252
|
+
/\bimport\s+(?!type\b)(?:gsap\b|\{[^}]*\bgsap\b[^}]*\})[^;]*?\bfrom\s*["']/.test(code) ||
|
|
253
|
+
/**
|
|
254
|
+
* Loaded on demand: `loadGsap().then(({ gsap }) => …)` or
|
|
255
|
+
* `const { gsap } = await import(…)`. A real project's brand loader did
|
|
256
|
+
* this, and was the one file in eleven the lifecycle rules skipped.
|
|
257
|
+
*/
|
|
258
|
+
/\(\s*\{[^}]*\bgsap\b[^}]*\}\s*\)\s*=>/.test(code) ||
|
|
259
|
+
/\{[^}]*\bgsap\b[^}]*\}\s*=\s*await\s+import\s*\(/.test(code),
|
|
253
260
|
};
|
|
254
261
|
}
|
|
255
262
|
|