@mehshekari/gsap-motion 2.1.0 → 3.1.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/NOTICE.md +59 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/skill/NOTICE.md +59 -0
- package/skill/SKILL.md +4 -3
- package/skill/example/cinematic-intro.md +9 -4
- package/skill/preset/cinematic.md +33 -1
- package/skill/preset/loader.md +6 -3
- package/skill/reference/core-gsap.md +5 -6
- package/skill/reference/svg.md +34 -2
- package/skill/reference/timeline.md +2 -1
- package/skill/scripts/audit-gsap.mjs +51 -14
- package/skill/scripts/lib/ast.mjs +527 -0
- package/skill/scripts/lib/rules.mjs +925 -316
- package/skill/scripts/lib/source.mjs +56 -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
|
}
|
|
@@ -113,109 +131,43 @@ export function lineAt(source, index) {
|
|
|
113
131
|
return line;
|
|
114
132
|
}
|
|
115
133
|
|
|
116
|
-
/**
|
|
117
|
-
* The `{ … }` block that opens at or after `from`, as a `[start, end]` pair.
|
|
118
|
-
*
|
|
119
|
-
* Used to ask "is this call inside that handler?" without a parser. Quotes are
|
|
120
|
-
* tracked so a brace inside a string cannot close the block early; comments are
|
|
121
|
-
* already gone by the time this runs.
|
|
122
|
-
*/
|
|
123
|
-
export function blockAfter(source, from) {
|
|
124
|
-
const start = source.indexOf("{", from);
|
|
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];
|
|
166
|
-
|
|
167
|
-
if (quote) {
|
|
168
|
-
if (c === "\\") i += 1;
|
|
169
|
-
else if (c === quote) quote = "";
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
if (c === '"' || c === "'" || c === "`") {
|
|
173
|
-
quote = c;
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
if (c === "(") depth += 1;
|
|
177
|
-
else if (c === ")") {
|
|
178
|
-
depth -= 1;
|
|
179
|
-
if (depth === 0) return [open, i];
|
|
180
|
-
}
|
|
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
134
|
/** A file plus everything the rules need, read once. */
|
|
214
135
|
export function load(path, root) {
|
|
215
136
|
const raw = readFileSync(path, "utf8");
|
|
216
137
|
const code = stripComments(raw);
|
|
217
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Parsed on first use, and once. Most files in a project never mention GSAP,
|
|
141
|
+
* and the run skips those before any rule asks for a tree. A file that does
|
|
142
|
+
* not parse keeps `ast: null` and says why in `parseError`, so it can be
|
|
143
|
+
* reported as not checked rather than passed as clean.
|
|
144
|
+
*/
|
|
145
|
+
let parsed;
|
|
146
|
+
const tree = () => {
|
|
147
|
+
if (parsed === undefined) {
|
|
148
|
+
try {
|
|
149
|
+
parsed = { ...parse(raw, path), error: null };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
parsed = {
|
|
152
|
+
ast: null,
|
|
153
|
+
comments: [],
|
|
154
|
+
error: { message: error.message, line: error.loc?.line ?? 1 },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return parsed;
|
|
159
|
+
};
|
|
160
|
+
|
|
218
161
|
return {
|
|
162
|
+
get ast() {
|
|
163
|
+
return tree().ast;
|
|
164
|
+
},
|
|
165
|
+
get comments() {
|
|
166
|
+
return tree().comments;
|
|
167
|
+
},
|
|
168
|
+
get parseError() {
|
|
169
|
+
return tree().error;
|
|
170
|
+
},
|
|
219
171
|
path,
|
|
220
172
|
display: relative(root, path).replace(/\\/g, "/"),
|
|
221
173
|
raw,
|
|
@@ -233,6 +185,5 @@ export function load(path, root) {
|
|
|
233
185
|
/\bfrom ["']@gsap\/react["']/.test(code),
|
|
234
186
|
isClient: /^\s*["']use client["']/m.test(code),
|
|
235
187
|
usesGsap: /\bfrom ["']gsap(?:\/|["'])/.test(code),
|
|
236
|
-
contexts: contextSpans(code),
|
|
237
188
|
};
|
|
238
189
|
}
|