@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 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.0 audit src --quiet"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mehshekari/gsap-motion",
3
- "version": "4.6.0",
3
+ "version": "4.6.1",
4
4
  "description": "Install the gsap-creative-animation Agent Skill, and audit GSAP animation for leaks, jank and accessibility failures — with npx.",
5
5
  "keywords": [
6
6
  "gsap",
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.0"
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 only: an alias such as `@/…` is not resolved, and a
119
- * file whose importers are unknown is judged on its own, as before.
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*["'](\.{1,2}\/[^"']+)["']/g)) {
126
- const base = resolve(dirname(source.path), specifier);
127
- const target = EXTENSIONS.map((ext) => byPath.get(base + ext)).find(Boolean);
128
- if (target && target !== source) target.importers.push(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 (file.importers?.length && file.importers.every((importer) => !importer.parseError && hasReducedBranch(importer))) {
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