@mehshekari/gsap-motion 3.0.0 → 3.1.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/NOTICE.md +59 -0
- package/README.md +4 -1
- package/package.json +1 -1
- package/skill/NOTICE.md +59 -0
- package/skill/SKILL.md +1 -1
- package/skill/scripts/audit-gsap.mjs +49 -50
- package/skill/scripts/lib/ast.mjs +527 -0
- package/skill/scripts/lib/audit.mjs +91 -0
- package/skill/scripts/lib/rules.mjs +890 -441
- package/skill/scripts/lib/source.mjs +79 -105
- package/skill/scripts/lib/vendor/parser.mjs +15 -0
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Source reading for the GSAP audit
|
|
2
|
+
* Source reading for the GSAP audit: finding the files, and reading each one
|
|
3
|
+
* once into what the rules need.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* otherwise be reported as doing exactly that.
|
|
5
|
+
* Rules read the syntax tree. A few facts about the whole file — is it React,
|
|
6
|
+
* does it use GSAP — are still read from its text with the comments blanked
|
|
7
|
+
* out, because well-commented animation code explains itself at length, and a
|
|
8
|
+
* docblock that mentions `@gsap/react` is not an import of it.
|
|
9
9
|
*/
|
|
10
10
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
11
11
|
import { extname, join, relative } from "node:path";
|
|
12
12
|
|
|
13
|
+
import { parse } from "./ast.mjs";
|
|
14
|
+
|
|
13
15
|
const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
|
|
14
16
|
const SKIP_DIRECTORIES = new Set([
|
|
15
17
|
"node_modules",
|
|
@@ -40,6 +42,22 @@ export function collect(root) {
|
|
|
40
42
|
return files.sort();
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Whether the character at `i` opens a string literal.
|
|
47
|
+
*
|
|
48
|
+
* An apostrophe between two word characters never does: that is JSX text —
|
|
49
|
+
* `<p>Don't</p>` — and in code it would be a syntax error. Read as an opening
|
|
50
|
+
* quote, it inverted every string and comment after it, so a comment survived
|
|
51
|
+
* stripping and a call's span ran past its own closing parenthesis. A backtick
|
|
52
|
+
* after a word character is left alone: that is a tagged template.
|
|
53
|
+
*/
|
|
54
|
+
export function opensString(source, i) {
|
|
55
|
+
const c = source[i];
|
|
56
|
+
if (c !== '"' && c !== "'" && c !== "`") return false;
|
|
57
|
+
const word = (ch) => ch !== undefined && /\w/.test(ch);
|
|
58
|
+
return !(c === "'" && word(source[i - 1]) && word(source[i + 1]));
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
/**
|
|
44
62
|
* Replaces comment bodies with spaces, keeping every newline and every offset.
|
|
45
63
|
*
|
|
@@ -68,7 +86,7 @@ export function stripComments(source) {
|
|
|
68
86
|
out[i] = " ";
|
|
69
87
|
out[i + 1] = " ";
|
|
70
88
|
i += 1;
|
|
71
|
-
} else if (
|
|
89
|
+
} else if (opensString(source, i)) {
|
|
72
90
|
mode = "string";
|
|
73
91
|
quote = c;
|
|
74
92
|
}
|
|
@@ -114,110 +132,53 @@ export function lineAt(source, index) {
|
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
/**
|
|
117
|
-
*
|
|
135
|
+
* A file plus everything the rules need, from its text.
|
|
118
136
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
137
|
+
* The command line reads the text from disk, through `load`. The ESLint plugin
|
|
138
|
+
* passes the text ESLint already holds, with the file's path, so both entry
|
|
139
|
+
* points run the rules on the same parse of the same characters.
|
|
122
140
|
*/
|
|
123
|
-
export function
|
|
124
|
-
const
|
|
125
|
-
if (start === -1) return null;
|
|
126
|
-
|
|
127
|
-
let depth = 0;
|
|
128
|
-
let quote = "";
|
|
129
|
-
|
|
130
|
-
for (let i = start; i < source.length; i += 1) {
|
|
131
|
-
const c = source[i];
|
|
132
|
-
|
|
133
|
-
if (quote) {
|
|
134
|
-
if (c === "\\") i += 1;
|
|
135
|
-
else if (c === quote) quote = "";
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (c === '"' || c === "'" || c === "`") {
|
|
139
|
-
quote = c;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (c === "{") depth += 1;
|
|
143
|
-
else if (c === "}") {
|
|
144
|
-
depth -= 1;
|
|
145
|
-
if (depth === 0) return [start, i];
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Whether `index` falls inside any of the given `[start, end]` spans. */
|
|
153
|
-
export const within = (spans, index) =>
|
|
154
|
-
spans.some(([start, end]) => index > start && index < end);
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* The `( … )` of a call, as a `[start, end]` pair, given the index of its
|
|
158
|
-
* opening paren. Quotes are tracked the same way `blockAfter` tracks them.
|
|
159
|
-
*/
|
|
160
|
-
export function parenSpan(source, open) {
|
|
161
|
-
let depth = 0;
|
|
162
|
-
let quote = "";
|
|
163
|
-
|
|
164
|
-
for (let i = open; i < source.length; i += 1) {
|
|
165
|
-
const c = source[i];
|
|
141
|
+
export function fromText(raw, path, display = path) {
|
|
142
|
+
const code = stripComments(raw);
|
|
166
143
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Parsed on first use, and once. Most files in a project never mention GSAP,
|
|
146
|
+
* and the run skips those before any rule asks for a tree. A file that does
|
|
147
|
+
* not parse keeps `ast: null` and says why in `parseError`, so it can be
|
|
148
|
+
* reported as not checked rather than passed as clean.
|
|
149
|
+
*/
|
|
150
|
+
let parsed;
|
|
151
|
+
const tree = () => {
|
|
152
|
+
if (parsed === undefined) {
|
|
153
|
+
try {
|
|
154
|
+
parsed = { ...parse(raw, path), error: null };
|
|
155
|
+
} catch (error) {
|
|
156
|
+
parsed = {
|
|
157
|
+
ast: null,
|
|
158
|
+
comments: [],
|
|
159
|
+
error: {
|
|
160
|
+
message: error.message,
|
|
161
|
+
line: error.loc?.line ?? 1,
|
|
162
|
+
column: error.loc?.column ?? 0,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
180
166
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Spans of every `useGSAP(…)`, `gsap.context(…)` and `contextSafe(…)` call.
|
|
188
|
-
*
|
|
189
|
-
* Anything created outside these is outside the context that reverts it, which
|
|
190
|
-
* is the single most common way an animation outlives its component.
|
|
191
|
-
*
|
|
192
|
-
* The span is the call's parentheses, not the first `{` after its name. The
|
|
193
|
-
* first brace failed silently twice over: a bodiless `useGSAP()` — the usual
|
|
194
|
-
* way to get `contextSafe` — claimed whatever block came next in the file, so an
|
|
195
|
-
* orphaned tween in the handler below it read as owned; and an expression-bodied
|
|
196
|
-
* `contextSafe(() => gsap.to(el, { … }))` measured as the tween's own vars
|
|
197
|
-
* object, which the call itself sits in front of.
|
|
198
|
-
*
|
|
199
|
-
* `contextSafe` counts because it is a context: a wrapped function's tweens are
|
|
200
|
-
* added to the one `useGSAP` created.
|
|
201
|
-
*/
|
|
202
|
-
export function contextSpans(code) {
|
|
203
|
-
const spans = [];
|
|
204
|
-
for (const match of code.matchAll(
|
|
205
|
-
/\b(?:useGSAP|gsap\.context|contextSafe)\s*\(/g,
|
|
206
|
-
)) {
|
|
207
|
-
const span = parenSpan(code, match.index + match[0].length - 1);
|
|
208
|
-
if (span) spans.push(span);
|
|
209
|
-
}
|
|
210
|
-
return spans;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** A file plus everything the rules need, read once. */
|
|
214
|
-
export function load(path, root) {
|
|
215
|
-
const raw = readFileSync(path, "utf8");
|
|
216
|
-
const code = stripComments(raw);
|
|
167
|
+
return parsed;
|
|
168
|
+
};
|
|
217
169
|
|
|
218
170
|
return {
|
|
171
|
+
get ast() {
|
|
172
|
+
return tree().ast;
|
|
173
|
+
},
|
|
174
|
+
get comments() {
|
|
175
|
+
return tree().comments;
|
|
176
|
+
},
|
|
177
|
+
get parseError() {
|
|
178
|
+
return tree().error;
|
|
179
|
+
},
|
|
219
180
|
path,
|
|
220
|
-
display
|
|
181
|
+
display,
|
|
221
182
|
raw,
|
|
222
183
|
code,
|
|
223
184
|
/**
|
|
@@ -233,6 +194,19 @@ export function load(path, root) {
|
|
|
233
194
|
/\bfrom ["']@gsap\/react["']/.test(code),
|
|
234
195
|
isClient: /^\s*["']use client["']/m.test(code),
|
|
235
196
|
usesGsap: /\bfrom ["']gsap(?:\/|["'])/.test(code),
|
|
236
|
-
contexts: contextSpans(code),
|
|
237
197
|
};
|
|
238
198
|
}
|
|
199
|
+
|
|
200
|
+
/** A file on disk, read once, shown by its path from `root`. */
|
|
201
|
+
export function load(path, root) {
|
|
202
|
+
return fromText(
|
|
203
|
+
readFileSync(path, "utf8"),
|
|
204
|
+
path,
|
|
205
|
+
relative(root, path).replace(/\\/g, "/"),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Whether `collect` reads files with this path's extension. */
|
|
210
|
+
export function hasSourceExtension(path) {
|
|
211
|
+
return SOURCE_EXTENSIONS.has(extname(path));
|
|
212
|
+
}
|