@mehshekari/gsap-motion 3.0.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.
@@ -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
- * Every helper here exists because naive matching produces false findings, and
5
- * a checker that cries wolf is one people switch off. Comments are stripped
6
- * before any rule runs — well-commented animation code explains itself at length, and
7
- * a docblock saying "do not use `gsap.to` in a pointermove handler" would
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 (c === '"' || c === "'" || c === "`") {
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
  }