@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.
@@ -0,0 +1,527 @@
1
+ /**
2
+ * The audit's syntax tree: parsing, walking, and the small questions rules ask
3
+ * of a tree.
4
+ *
5
+ * The text-matching engine answered "is this call inside that handler?" by
6
+ * counting braces, and every wrong answer it gave came from not knowing what a
7
+ * brace, a quote or a name actually was. A tree knows. The parser is vendored
8
+ * (see `vendor/parser.mjs` and `scripts/vendor-parser.mjs` in the repository),
9
+ * so this still installs nothing.
10
+ */
11
+ import { Parser, tsPlugin } from "./vendor/parser.mjs";
12
+
13
+ const OPTIONS = {
14
+ ecmaVersion: "latest",
15
+ sourceType: "module",
16
+ locations: true,
17
+ allowHashBang: true,
18
+ };
19
+
20
+ /**
21
+ * `.ts` is read without JSX, because there `<T>(x) => x` is a generic and
22
+ * `<Type>value` a cast; every other extension is read with it. A `.js` file
23
+ * with JSX is common enough, and JSX never changes how plain JavaScript parses.
24
+ */
25
+ const TYPESCRIPT = Parser.extend(tsPlugin());
26
+ const TYPESCRIPT_JSX = Parser.extend(tsPlugin({ jsx: true }));
27
+
28
+ /**
29
+ * Parses `source` as the grammar its `path` implies.
30
+ *
31
+ * Returns `{ ast, comments }`, or throws the parser's error, which carries
32
+ * `loc.line`. Offsets on every node index into `source` itself, so a node's
33
+ * `start` works with `lineAt` exactly as a text-match index did.
34
+ */
35
+ export function parse(source, path) {
36
+ const comments = [];
37
+ const parser = /\.[mc]?ts$/.test(path) ? TYPESCRIPT : TYPESCRIPT_JSX;
38
+ const ast = parser.parse(source, { ...OPTIONS, onComment: comments });
39
+ return { ast, comments };
40
+ }
41
+
42
+ const isNode = (value) =>
43
+ value !== null && typeof value === "object" && typeof value.type === "string";
44
+
45
+ /**
46
+ * Calls `visit(node, ancestors)` for every node, parents before children, in
47
+ * source order. `ancestors` is nearest-last and is the live stack: copy it
48
+ * before keeping it.
49
+ */
50
+ export function walk(root, visit) {
51
+ const ancestors = [];
52
+
53
+ const step = (node) => {
54
+ visit(node, ancestors);
55
+ ancestors.push(node);
56
+ for (const key of Object.keys(node)) {
57
+ if (key === "loc") continue;
58
+ const value = node[key];
59
+ if (Array.isArray(value)) {
60
+ for (const child of value) if (isNode(child)) step(child);
61
+ } else if (isNode(value)) {
62
+ step(value);
63
+ }
64
+ }
65
+ ancestors.pop();
66
+ };
67
+
68
+ step(root);
69
+ }
70
+
71
+ /** Every node for which `test(node, ancestors)` holds, in source order. */
72
+ export function findAll(root, test) {
73
+ const found = [];
74
+ walk(root, (node, ancestors) => {
75
+ if (test(node, ancestors)) found.push(node);
76
+ });
77
+ return found;
78
+ }
79
+
80
+ /** Wrappers that change a value's type or grouping but not what it is. */
81
+ const TRANSPARENT = new Set([
82
+ "ParenthesizedExpression",
83
+ "TSNonNullExpression",
84
+ "TSAsExpression",
85
+ "TSSatisfiesExpression",
86
+ "TSTypeAssertion",
87
+ "ChainExpression",
88
+ ]);
89
+
90
+ /** `node` with type assertions, non-null marks and parentheses taken off. */
91
+ export function unwrap(node) {
92
+ let current = node;
93
+ while (current && TRANSPARENT.has(current.type)) current = current.expression;
94
+ return current;
95
+ }
96
+
97
+ /**
98
+ * The dotted name a plain reference spells — `gsap`, `gsap.to`,
99
+ * `this.tl.to` — or `null` when any part is computed or not a name.
100
+ */
101
+ export function dottedName(node) {
102
+ const target = unwrap(node);
103
+ if (!target) return null;
104
+ if (target.type === "Identifier") return target.name;
105
+ if (target.type === "ThisExpression") return "this";
106
+ if (
107
+ target.type === "MemberExpression" &&
108
+ !target.computed &&
109
+ target.property.type === "Identifier"
110
+ ) {
111
+ const object = dottedName(target.object);
112
+ return object === null ? null : `${object}.${target.property.name}`;
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /** The dotted name of a call's callee: `gsap.to` for `gsap.to(el, {})`. */
118
+ export const calleeName = (call) =>
119
+ call.type === "CallExpression" ? dottedName(call.callee) : null;
120
+
121
+ /**
122
+ * The string a node spells with nothing computed: a string literal, or a
123
+ * template literal with no `${}` in it. `null` for anything else.
124
+ */
125
+ export function staticString(node) {
126
+ const target = unwrap(node);
127
+ if (!target) return null;
128
+ if (target.type === "Literal" && typeof target.value === "string") {
129
+ return target.value;
130
+ }
131
+ if (target.type === "TemplateLiteral" && target.expressions.length === 0) {
132
+ return target.quasis[0].value.cooked ?? null;
133
+ }
134
+ return null;
135
+ }
136
+
137
+ /** The name of a non-computed property key: `ease` in `{ ease: "none" }`. */
138
+ export function keyName(property) {
139
+ if (property.type !== "Property" || property.computed) return null;
140
+ if (property.key.type === "Identifier") return property.key.name;
141
+ if (property.key.type === "Literal") return String(property.key.value);
142
+ return null;
143
+ }
144
+
145
+ /** The module specifier of an import or re-export, or `null`. */
146
+ export function moduleSource(node) {
147
+ if (
148
+ node.type === "ImportDeclaration" ||
149
+ node.type === "ExportAllDeclaration" ||
150
+ (node.type === "ExportNamedDeclaration" && node.source)
151
+ ) {
152
+ return node.source;
153
+ }
154
+ return null;
155
+ }
156
+
157
+ /** GSAP's own tween-creating calls: `gsap.to`, `.from`, `.fromTo`, `.timeline`. */
158
+ export const TWEEN_METHODS = new Set(["to", "from", "fromTo", "timeline"]);
159
+
160
+ /** Whether `node` is `gsap.<method>(…)` for one of `methods`. */
161
+ export function isGsapCall(node, methods = TWEEN_METHODS) {
162
+ const name = calleeName(node);
163
+ return (
164
+ name !== null &&
165
+ name.startsWith("gsap.") &&
166
+ name.split(".").length === 2 &&
167
+ methods.has(name.slice(5))
168
+ );
169
+ }
170
+
171
+ /** The property of an object literal with key `name`, or `undefined`. */
172
+ export const propertyOf = (object, name) =>
173
+ object?.properties?.find((property) => keyName(property) === name);
174
+
175
+ /** The number a literal spells, a negative one included: `-1` for `repeat: -1`. */
176
+ export function numberValue(node) {
177
+ const target = unwrap(node);
178
+ if (target?.type === "Literal" && typeof target.value === "number") {
179
+ return target.value;
180
+ }
181
+ if (target?.type === "UnaryExpression" && target.operator === "-") {
182
+ const value = numberValue(target.argument);
183
+ return value === null ? null : -value;
184
+ }
185
+ return null;
186
+ }
187
+
188
+ const ON_GSAP = new Set(["to", "from", "fromTo", "set", "timeline"]);
189
+ const ON_ANYTHING = new Set(["to", "from", "fromTo"]);
190
+
191
+ /**
192
+ * The method of a call that creates a tween or adds one: `gsap.to`, `.from`,
193
+ * `.fromTo`, `.set` or `.timeline`, and `.to`, `.from` or `.fromTo` on any other
194
+ * receiver — a timeline variable, `this.tl`, a chain. `null` for anything else.
195
+ */
196
+ export function tweenMethod(node) {
197
+ if (node.type !== "CallExpression") return null;
198
+ const callee = unwrap(node.callee);
199
+ if (
200
+ callee?.type !== "MemberExpression" ||
201
+ callee.computed ||
202
+ callee.property.type !== "Identifier"
203
+ ) {
204
+ return null;
205
+ }
206
+ const method = callee.property.name;
207
+ const allowed = dottedName(callee.object) === "gsap" ? ON_GSAP : ON_ANYTHING;
208
+ return allowed.has(method) ? method : null;
209
+ }
210
+
211
+ const argumentObject = (call, index) => {
212
+ const node = unwrap(call.arguments[index]);
213
+ return node?.type === "ObjectExpression" ? node : null;
214
+ };
215
+
216
+ /**
217
+ * The vars object that holds a tween's own settings — `repeat`, `ease`,
218
+ * `yoyo`: the second argument, a `fromTo`'s third, a timeline's first. `null`
219
+ * when it is not an object literal.
220
+ */
221
+ export const ownVars = (call, method) =>
222
+ argumentObject(call, method === "fromTo" ? 2 : method === "timeline" ? 0 : 1);
223
+
224
+ /**
225
+ * Every vars object a tween call carries, as object literals: both of a
226
+ * `fromTo`'s, a timeline's own, or the tween's.
227
+ */
228
+ export function varsObjects(call, method) {
229
+ const indexes = method === "fromTo" ? [1, 2] : method === "timeline" ? [0] : [1];
230
+ return indexes.map((index) => argumentObject(call, index)).filter(Boolean);
231
+ }
232
+
233
+ const FUNCTIONS = new Set([
234
+ "ArrowFunctionExpression",
235
+ "FunctionExpression",
236
+ "FunctionDeclaration",
237
+ ]);
238
+
239
+ /** Whether `node` is a function of any form. */
240
+ export const isFunction = (node) => FUNCTIONS.has(node?.type);
241
+
242
+ /** Whether `inner` lies within `outer`'s source range. */
243
+ export const contains = (outer, inner) =>
244
+ outer.start <= inner.start && inner.end <= outer.end;
245
+
246
+ /**
247
+ * The function `node` stands for: itself when it is one, or, for a plain name,
248
+ * the single function declared under that name anywhere in the file — a
249
+ * `function` declaration or a `const` holding one.
250
+ *
251
+ * Names are not resolved through scopes. A name declared twice, or not at all,
252
+ * returns `null`: an ambiguous handler is left unread rather than guessed at.
253
+ */
254
+ export function resolveFunction(root, node) {
255
+ const target = unwrap(node);
256
+ if (isFunction(target)) return target;
257
+ if (target?.type !== "Identifier") return null;
258
+
259
+ const declarations = findAll(
260
+ root,
261
+ (n) =>
262
+ (n.type === "FunctionDeclaration" && n.id?.name === target.name) ||
263
+ (n.type === "VariableDeclarator" &&
264
+ n.id.type === "Identifier" &&
265
+ n.id.name === target.name &&
266
+ isFunction(unwrap(n.init))),
267
+ );
268
+ if (declarations.length !== 1) return null;
269
+ const [declaration] = declarations;
270
+ return declaration.type === "FunctionDeclaration"
271
+ ? declaration
272
+ : unwrap(declaration.init);
273
+ }
274
+
275
+ const PARENTS = new WeakMap();
276
+
277
+ /** The node that directly holds `node` in `root`'s tree, or `null`. Mapped once per tree. */
278
+ export function parentOf(root, node) {
279
+ let parents = PARENTS.get(root);
280
+ if (!parents) {
281
+ parents = new WeakMap();
282
+ walk(root, (child, ancestors) => {
283
+ if (ancestors.length) parents.set(child, ancestors[ancestors.length - 1]);
284
+ });
285
+ PARENTS.set(root, parents);
286
+ }
287
+ return parents.get(node) ?? null;
288
+ }
289
+
290
+ /** Every node that holds `node`, nearest first. */
291
+ export function ancestorsOf(root, node) {
292
+ const found = [];
293
+ for (let parent = parentOf(root, node); parent; parent = parentOf(root, parent)) {
294
+ found.push(parent);
295
+ }
296
+ return found;
297
+ }
298
+
299
+ /** The method a call makes on a receiver — `to` for `tl.to(…)` — or `null`. */
300
+ export function methodName(node) {
301
+ if (node?.type !== "CallExpression") return null;
302
+ const callee = unwrap(node.callee);
303
+ return callee?.type === "MemberExpression" &&
304
+ !callee.computed &&
305
+ callee.property.type === "Identifier"
306
+ ? callee.property.name
307
+ : null;
308
+ }
309
+
310
+ /** The innermost call of a method chain: `tl.addLabel("a")` in `tl.addLabel("a").to(…)`. */
311
+ export function chainStart(call) {
312
+ let current = call;
313
+ for (;;) {
314
+ const callee = unwrap(current.callee);
315
+ const object = callee?.type === "MemberExpression" ? unwrap(callee.object) : null;
316
+ if (object?.type !== "CallExpression") return current;
317
+ current = object;
318
+ }
319
+ }
320
+
321
+ /** What a method chain is called on, as a dotted name: `tl` for `tl.addLabel("a").to(…)`. */
322
+ export function chainReceiver(call) {
323
+ const callee = unwrap(chainStart(call).callee);
324
+ return callee?.type === "MemberExpression" ? dottedName(callee.object) : null;
325
+ }
326
+
327
+ /** The nearest function holding `node`, or the program: where a name declared at `node` is visible. */
328
+ export function scopeOf(root, node) {
329
+ for (let parent = parentOf(root, node); parent; parent = parentOf(root, parent)) {
330
+ if (isFunction(parent) || parent.type === "Program") return parent;
331
+ }
332
+ return root;
333
+ }
334
+
335
+ /**
336
+ * The name the value built at `node` is kept under, through any chain called on
337
+ * it — `tl` for `const tl = gsap.timeline().to(…)`, `split.current` for
338
+ * `split.current = new SplitText(…)` — as `{ name, holder }`, or `null`.
339
+ */
340
+ export function keptUnder(root, node) {
341
+ let current = node;
342
+ let parent = parentOf(root, current);
343
+ while (
344
+ parent &&
345
+ (TRANSPARENT.has(parent.type) ||
346
+ (parent.type === "MemberExpression" && parent.object === current) ||
347
+ (parent.type === "CallExpression" && parent.callee === current))
348
+ ) {
349
+ current = parent;
350
+ parent = parentOf(root, current);
351
+ }
352
+ if (parent?.type === "VariableDeclarator" && parent.init === current) {
353
+ return { name: parent.id.type === "Identifier" ? parent.id.name : null, holder: parent };
354
+ }
355
+ if (parent?.type === "AssignmentExpression" && parent.right === current) {
356
+ return { name: dottedName(parent.left), holder: parent };
357
+ }
358
+ return null;
359
+ }
360
+
361
+ /**
362
+ * Every call that adds to or configures the timeline `timeline` creates: the
363
+ * chain on the call itself, and every chain on the name it is kept under —
364
+ * after that declaration, inside the scope that holds it, and not where the
365
+ * same name is declared again. Ordered by where each call ends, so a chain
366
+ * reads from the inside out.
367
+ */
368
+ export function timelineLinks(root, timeline) {
369
+ const calls = findAll(root, (node) => node !== timeline && methodName(node) !== null);
370
+ const links = calls.filter((call) => chainStart(call) === timeline);
371
+
372
+ const kept = keptUnder(root, timeline);
373
+ if (kept?.name) {
374
+ const scope = scopeOf(root, kept.holder);
375
+ const redeclarations = findAll(
376
+ root,
377
+ (node) =>
378
+ node !== kept.holder &&
379
+ node.type === "VariableDeclarator" &&
380
+ node.id.type === "Identifier" &&
381
+ node.id.name === kept.name,
382
+ );
383
+
384
+ for (const call of calls) {
385
+ if (links.includes(call) || call.start < kept.holder.end) continue;
386
+ if (!contains(scope, call) || chainReceiver(call) !== kept.name) continue;
387
+ const shadowed = redeclarations.some((declaration) => {
388
+ const region = scopeOf(root, declaration);
389
+ if (!contains(region, call) || declaration.start > call.start) return false;
390
+ return region === scope
391
+ ? declaration.start > kept.holder.end
392
+ : contains(scope, region);
393
+ });
394
+ if (!shadowed) links.push(call);
395
+ }
396
+ }
397
+
398
+ return links.sort((a, b) => a.end - b.end);
399
+ }
400
+
401
+ /** Whether an identifier is a use of a name, rather than a name being declared or a property key. */
402
+ function isReference(root, id) {
403
+ const parent = parentOf(root, id);
404
+ if (!parent) return true;
405
+ switch (parent.type) {
406
+ case "VariableDeclarator":
407
+ return parent.id !== id;
408
+ case "FunctionDeclaration":
409
+ case "FunctionExpression":
410
+ case "ArrowFunctionExpression":
411
+ return parent.id !== id && !parent.params.includes(id);
412
+ case "Property":
413
+ case "PropertyDefinition":
414
+ case "MethodDefinition":
415
+ return !(parent.key === id && !parent.computed && !parent.shorthand);
416
+ case "MemberExpression":
417
+ return !(parent.property === id && !parent.computed);
418
+ case "ImportSpecifier":
419
+ case "ImportDefaultSpecifier":
420
+ case "ImportNamespaceSpecifier":
421
+ case "ExportSpecifier":
422
+ case "LabeledStatement":
423
+ case "BreakStatement":
424
+ case "ContinueStatement":
425
+ return false;
426
+ default:
427
+ return true;
428
+ }
429
+ }
430
+
431
+ /**
432
+ * A test for whether a node runs inside a GSAP context: within the arguments
433
+ * of a `useGSAP`, `gsap.context` or `contextSafe` call, under whatever local
434
+ * names the file imports or destructures them as — or within a function the
435
+ * file declares once whose every use is a call from inside a context.
436
+ *
437
+ * A function used any other way — passed as a prop, stored, returned — can run
438
+ * from anywhere, so it is not counted as inside, however it is called here.
439
+ */
440
+ export function gsapContexts(root) {
441
+ const hooks = new Set(["useGSAP"]);
442
+ for (const declaration of root.body) {
443
+ if (declaration.type !== "ImportDeclaration") continue;
444
+ if (declaration.source.value !== "@gsap/react") continue;
445
+ for (const specifier of declaration.specifiers) {
446
+ const imported = specifier.imported?.name ?? specifier.imported?.value;
447
+ if (specifier.type === "ImportSpecifier" && imported === "useGSAP") {
448
+ hooks.add(specifier.local.name);
449
+ }
450
+ }
451
+ }
452
+
453
+ const bareName = (call) => {
454
+ const callee = unwrap(call.callee);
455
+ return callee?.type === "Identifier" ? callee.name : null;
456
+ };
457
+ const isHook = (node) => node?.type === "CallExpression" && hooks.has(bareName(node));
458
+
459
+ const safe = new Set(["contextSafe"]);
460
+ const destructured = findAll(
461
+ root,
462
+ (node) =>
463
+ node.type === "VariableDeclarator" &&
464
+ node.id.type === "ObjectPattern" &&
465
+ isHook(unwrap(node.init)),
466
+ );
467
+ for (const declarator of destructured) {
468
+ for (const property of declarator.id.properties) {
469
+ if (keyName(property) === "contextSafe" && property.value.type === "Identifier") {
470
+ safe.add(property.value.name);
471
+ }
472
+ }
473
+ }
474
+
475
+ const scopes = findAll(
476
+ root,
477
+ (node) =>
478
+ node.type === "CallExpression" &&
479
+ (isHook(node) || calleeName(node) === "gsap.context" || safe.has(bareName(node))),
480
+ ).flatMap((call) => call.arguments);
481
+
482
+ const counts = new Map();
483
+ const functions = new Map();
484
+ const declared = findAll(
485
+ root,
486
+ (node) =>
487
+ (node.type === "FunctionDeclaration" && node.id) ||
488
+ (node.type === "VariableDeclarator" &&
489
+ node.id.type === "Identifier" &&
490
+ isFunction(unwrap(node.init))),
491
+ );
492
+ for (const node of declared) {
493
+ const { name } = node.id;
494
+ counts.set(name, (counts.get(name) ?? 0) + 1);
495
+ functions.set(name, node.type === "FunctionDeclaration" ? node : unwrap(node.init));
496
+ }
497
+
498
+ const contained = new Set();
499
+ const inside = (node) =>
500
+ scopes.some((argument) => contains(argument, node)) ||
501
+ [...contained].some((fn) => contains(fn, node));
502
+ const identifiers = findAll(root, (node) => node.type === "Identifier");
503
+
504
+ for (let changed = true; changed; ) {
505
+ changed = false;
506
+ for (const [name, fn] of functions) {
507
+ if (counts.get(name) !== 1 || contained.has(fn)) continue;
508
+ const uses = identifiers.filter((id) => id.name === name && isReference(root, id));
509
+ if (!uses.length) continue;
510
+ const calledFromInside = uses.every((id) => {
511
+ const parent = parentOf(root, id);
512
+ return (
513
+ parent?.type === "CallExpression" &&
514
+ parent.callee === id &&
515
+ !contains(fn, parent) &&
516
+ inside(parent)
517
+ );
518
+ });
519
+ if (calledFromInside) {
520
+ contained.add(fn);
521
+ changed = true;
522
+ }
523
+ }
524
+ }
525
+
526
+ return inside;
527
+ }