@hyperframes/core 0.7.13 → 0.7.15

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.
Files changed (238) hide show
  1. package/dist/compiler/staticGuard.js +1 -1
  2. package/dist/compiler/staticGuard.js.map +1 -1
  3. package/dist/core.types.d.ts +2 -369
  4. package/dist/core.types.d.ts.map +1 -1
  5. package/dist/core.types.js +1 -84
  6. package/dist/core.types.js.map +1 -1
  7. package/dist/generators/hyperframes.d.ts +1 -1
  8. package/dist/generators/hyperframes.d.ts.map +1 -1
  9. package/dist/generators/hyperframes.js +1 -1
  10. package/dist/generators/hyperframes.js.map +1 -1
  11. package/dist/index.d.ts +5 -7
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +7 -4
  14. package/dist/index.js.map +1 -1
  15. package/dist/lint/index.d.ts +2 -2
  16. package/dist/lint/index.d.ts.map +1 -1
  17. package/dist/lint/index.js +2 -1
  18. package/dist/lint/index.js.map +1 -1
  19. package/dist/parsers/gsapConstants.d.ts +2 -12
  20. package/dist/parsers/gsapConstants.d.ts.map +1 -1
  21. package/dist/parsers/gsapConstants.js +2 -110
  22. package/dist/parsers/gsapConstants.js.map +1 -1
  23. package/dist/parsers/gsapParserAcorn.d.ts +2 -48
  24. package/dist/parsers/gsapParserAcorn.d.ts.map +1 -1
  25. package/dist/parsers/gsapParserAcorn.js +2 -1057
  26. package/dist/parsers/gsapParserAcorn.js.map +1 -1
  27. package/dist/parsers/gsapParserExports.d.ts +2 -20
  28. package/dist/parsers/gsapParserExports.d.ts.map +1 -1
  29. package/dist/parsers/gsapParserExports.js +2 -8
  30. package/dist/parsers/gsapParserExports.js.map +1 -1
  31. package/dist/parsers/gsapWriterAcorn.d.ts +2 -90
  32. package/dist/parsers/gsapWriterAcorn.d.ts.map +1 -1
  33. package/dist/parsers/gsapWriterAcorn.js +2 -2016
  34. package/dist/parsers/gsapWriterAcorn.js.map +1 -1
  35. package/dist/parsers/hfIds.d.ts +2 -16
  36. package/dist/parsers/hfIds.d.ts.map +1 -1
  37. package/dist/parsers/hfIds.js +2 -127
  38. package/dist/parsers/hfIds.js.map +1 -1
  39. package/dist/parsers/springEase.d.ts +2 -28
  40. package/dist/parsers/springEase.d.ts.map +1 -1
  41. package/dist/parsers/springEase.js +2 -69
  42. package/dist/parsers/springEase.js.map +1 -1
  43. package/dist/slideshow/index.d.ts +1 -0
  44. package/dist/slideshow/index.d.ts.map +1 -1
  45. package/dist/slideshow/index.js +1 -0
  46. package/dist/slideshow/index.js.map +1 -1
  47. package/dist/studio-api/helpers/draftMarkers.d.ts +2 -10
  48. package/dist/studio-api/helpers/draftMarkers.d.ts.map +1 -1
  49. package/dist/studio-api/helpers/draftMarkers.js +2 -10
  50. package/dist/studio-api/helpers/draftMarkers.js.map +1 -1
  51. package/dist/studio-api/helpers/finiteMutation.d.ts +2 -10
  52. package/dist/studio-api/helpers/finiteMutation.d.ts.map +1 -1
  53. package/dist/studio-api/helpers/finiteMutation.js +2 -20
  54. package/dist/studio-api/helpers/finiteMutation.js.map +1 -1
  55. package/dist/studio-api/helpers/manualEditsRenderScript.d.ts +2 -12
  56. package/dist/studio-api/helpers/manualEditsRenderScript.d.ts.map +1 -1
  57. package/dist/studio-api/helpers/manualEditsRenderScript.js +2 -632
  58. package/dist/studio-api/helpers/manualEditsRenderScript.js.map +1 -1
  59. package/dist/studio-api/helpers/screenshotClip.d.ts +2 -7
  60. package/dist/studio-api/helpers/screenshotClip.d.ts.map +1 -1
  61. package/dist/studio-api/helpers/screenshotClip.js +2 -21
  62. package/dist/studio-api/helpers/screenshotClip.js.map +1 -1
  63. package/dist/studio-api/helpers/studioMotionRenderScript.d.ts +2 -8
  64. package/dist/studio-api/helpers/studioMotionRenderScript.d.ts.map +1 -1
  65. package/dist/studio-api/helpers/studioMotionRenderScript.js +2 -207
  66. package/dist/studio-api/helpers/studioMotionRenderScript.js.map +1 -1
  67. package/dist/studio-api/index.d.ts +2 -9
  68. package/dist/studio-api/index.d.ts.map +1 -1
  69. package/dist/studio-api/index.js +2 -8
  70. package/dist/studio-api/index.js.map +1 -1
  71. package/package.json +16 -18
  72. package/dist/generated/runtime-inline.js +0 -11
  73. package/dist/hyperframe.manifest.json +0 -22
  74. package/dist/hyperframe.runtime.mjs +0 -325
  75. package/dist/lint/context.d.ts +0 -17
  76. package/dist/lint/context.d.ts.map +0 -1
  77. package/dist/lint/context.js +0 -37
  78. package/dist/lint/context.js.map +0 -1
  79. package/dist/lint/hyperframeLinter.d.ts +0 -14
  80. package/dist/lint/hyperframeLinter.d.ts.map +0 -1
  81. package/dist/lint/hyperframeLinter.js +0 -132
  82. package/dist/lint/hyperframeLinter.js.map +0 -1
  83. package/dist/lint/rules/adapters.d.ts +0 -3
  84. package/dist/lint/rules/adapters.d.ts.map +0 -1
  85. package/dist/lint/rules/adapters.js +0 -40
  86. package/dist/lint/rules/adapters.js.map +0 -1
  87. package/dist/lint/rules/captions.d.ts +0 -3
  88. package/dist/lint/rules/captions.d.ts.map +0 -1
  89. package/dist/lint/rules/captions.js +0 -242
  90. package/dist/lint/rules/captions.js.map +0 -1
  91. package/dist/lint/rules/composition.d.ts +0 -5
  92. package/dist/lint/rules/composition.d.ts.map +0 -1
  93. package/dist/lint/rules/composition.js +0 -694
  94. package/dist/lint/rules/composition.js.map +0 -1
  95. package/dist/lint/rules/core.d.ts +0 -3
  96. package/dist/lint/rules/core.d.ts.map +0 -1
  97. package/dist/lint/rules/core.js +0 -432
  98. package/dist/lint/rules/core.js.map +0 -1
  99. package/dist/lint/rules/fonts.d.ts +0 -3
  100. package/dist/lint/rules/fonts.d.ts.map +0 -1
  101. package/dist/lint/rules/fonts.js +0 -205
  102. package/dist/lint/rules/fonts.js.map +0 -1
  103. package/dist/lint/rules/gsap.d.ts +0 -4
  104. package/dist/lint/rules/gsap.d.ts.map +0 -1
  105. package/dist/lint/rules/gsap.js +0 -1039
  106. package/dist/lint/rules/gsap.js.map +0 -1
  107. package/dist/lint/rules/media.d.ts +0 -3
  108. package/dist/lint/rules/media.d.ts.map +0 -1
  109. package/dist/lint/rules/media.js +0 -491
  110. package/dist/lint/rules/media.js.map +0 -1
  111. package/dist/lint/rules/slideshow.d.ts +0 -4
  112. package/dist/lint/rules/slideshow.d.ts.map +0 -1
  113. package/dist/lint/rules/slideshow.js +0 -74
  114. package/dist/lint/rules/slideshow.js.map +0 -1
  115. package/dist/lint/rules/textures.d.ts +0 -3
  116. package/dist/lint/rules/textures.d.ts.map +0 -1
  117. package/dist/lint/rules/textures.js +0 -192
  118. package/dist/lint/rules/textures.js.map +0 -1
  119. package/dist/lint/types.d.ts +0 -36
  120. package/dist/lint/types.d.ts.map +0 -1
  121. package/dist/lint/types.js +0 -2
  122. package/dist/lint/types.js.map +0 -1
  123. package/dist/lint/utils.d.ts +0 -55
  124. package/dist/lint/utils.d.ts.map +0 -1
  125. package/dist/lint/utils.js +0 -264
  126. package/dist/lint/utils.js.map +0 -1
  127. package/dist/parsers/gsapInline.d.ts +0 -47
  128. package/dist/parsers/gsapInline.d.ts.map +0 -1
  129. package/dist/parsers/gsapInline.js +0 -513
  130. package/dist/parsers/gsapInline.js.map +0 -1
  131. package/dist/parsers/gsapParser.d.ts +0 -158
  132. package/dist/parsers/gsapParser.d.ts.map +0 -1
  133. package/dist/parsers/gsapParser.js +0 -2634
  134. package/dist/parsers/gsapParser.js.map +0 -1
  135. package/dist/parsers/gsapParser.test-helpers.d.ts +0 -49
  136. package/dist/parsers/gsapParser.test-helpers.d.ts.map +0 -1
  137. package/dist/parsers/gsapParser.test-helpers.js +0 -97
  138. package/dist/parsers/gsapParser.test-helpers.js.map +0 -1
  139. package/dist/parsers/gsapSerialize.d.ts +0 -191
  140. package/dist/parsers/gsapSerialize.d.ts.map +0 -1
  141. package/dist/parsers/gsapSerialize.js +0 -410
  142. package/dist/parsers/gsapSerialize.js.map +0 -1
  143. package/dist/parsers/gsapUnroll.d.ts +0 -7
  144. package/dist/parsers/gsapUnroll.d.ts.map +0 -1
  145. package/dist/parsers/gsapUnroll.js +0 -137
  146. package/dist/parsers/gsapUnroll.js.map +0 -1
  147. package/dist/parsers/htmlParser.d.ts +0 -27
  148. package/dist/parsers/htmlParser.d.ts.map +0 -1
  149. package/dist/parsers/htmlParser.js +0 -731
  150. package/dist/parsers/htmlParser.js.map +0 -1
  151. package/dist/studio-api/createStudioApi.d.ts +0 -10
  152. package/dist/studio-api/createStudioApi.d.ts.map +0 -1
  153. package/dist/studio-api/createStudioApi.js +0 -32
  154. package/dist/studio-api/createStudioApi.js.map +0 -1
  155. package/dist/studio-api/helpers/backupJournal.d.ts +0 -9
  156. package/dist/studio-api/helpers/backupJournal.d.ts.map +0 -1
  157. package/dist/studio-api/helpers/backupJournal.js +0 -81
  158. package/dist/studio-api/helpers/backupJournal.js.map +0 -1
  159. package/dist/studio-api/helpers/hfIdPersist.d.ts +0 -11
  160. package/dist/studio-api/helpers/hfIdPersist.d.ts.map +0 -1
  161. package/dist/studio-api/helpers/hfIdPersist.js +0 -39
  162. package/dist/studio-api/helpers/hfIdPersist.js.map +0 -1
  163. package/dist/studio-api/helpers/mediaValidation.d.ts +0 -20
  164. package/dist/studio-api/helpers/mediaValidation.d.ts.map +0 -1
  165. package/dist/studio-api/helpers/mediaValidation.js +0 -56
  166. package/dist/studio-api/helpers/mediaValidation.js.map +0 -1
  167. package/dist/studio-api/helpers/mime.d.ts +0 -4
  168. package/dist/studio-api/helpers/mime.d.ts.map +0 -1
  169. package/dist/studio-api/helpers/mime.js +0 -39
  170. package/dist/studio-api/helpers/mime.js.map +0 -1
  171. package/dist/studio-api/helpers/previewAdapter.d.ts +0 -43
  172. package/dist/studio-api/helpers/previewAdapter.d.ts.map +0 -1
  173. package/dist/studio-api/helpers/previewAdapter.js +0 -118
  174. package/dist/studio-api/helpers/previewAdapter.js.map +0 -1
  175. package/dist/studio-api/helpers/projectSignature.d.ts +0 -5
  176. package/dist/studio-api/helpers/projectSignature.d.ts.map +0 -1
  177. package/dist/studio-api/helpers/projectSignature.js +0 -154
  178. package/dist/studio-api/helpers/projectSignature.js.map +0 -1
  179. package/dist/studio-api/helpers/safePath.d.ts +0 -12
  180. package/dist/studio-api/helpers/safePath.d.ts.map +0 -1
  181. package/dist/studio-api/helpers/safePath.js +0 -38
  182. package/dist/studio-api/helpers/safePath.js.map +0 -1
  183. package/dist/studio-api/helpers/sourceMutation.d.ts +0 -28
  184. package/dist/studio-api/helpers/sourceMutation.d.ts.map +0 -1
  185. package/dist/studio-api/helpers/sourceMutation.js +0 -339
  186. package/dist/studio-api/helpers/sourceMutation.js.map +0 -1
  187. package/dist/studio-api/helpers/subComposition.d.ts +0 -20
  188. package/dist/studio-api/helpers/subComposition.d.ts.map +0 -1
  189. package/dist/studio-api/helpers/subComposition.js +0 -289
  190. package/dist/studio-api/helpers/subComposition.js.map +0 -1
  191. package/dist/studio-api/helpers/waveform.d.ts +0 -4
  192. package/dist/studio-api/helpers/waveform.d.ts.map +0 -1
  193. package/dist/studio-api/helpers/waveform.js +0 -80
  194. package/dist/studio-api/helpers/waveform.js.map +0 -1
  195. package/dist/studio-api/routes/files.d.ts +0 -4
  196. package/dist/studio-api/routes/files.d.ts.map +0 -1
  197. package/dist/studio-api/routes/files.js +0 -1200
  198. package/dist/studio-api/routes/files.js.map +0 -1
  199. package/dist/studio-api/routes/fonts.d.ts +0 -3
  200. package/dist/studio-api/routes/fonts.d.ts.map +0 -1
  201. package/dist/studio-api/routes/fonts.js +0 -157
  202. package/dist/studio-api/routes/fonts.js.map +0 -1
  203. package/dist/studio-api/routes/lint.d.ts +0 -4
  204. package/dist/studio-api/routes/lint.d.ts.map +0 -1
  205. package/dist/studio-api/routes/lint.js +0 -29
  206. package/dist/studio-api/routes/lint.js.map +0 -1
  207. package/dist/studio-api/routes/preview.d.ts +0 -4
  208. package/dist/studio-api/routes/preview.d.ts.map +0 -1
  209. package/dist/studio-api/routes/preview.js +0 -331
  210. package/dist/studio-api/routes/preview.js.map +0 -1
  211. package/dist/studio-api/routes/projects.d.ts +0 -4
  212. package/dist/studio-api/routes/projects.d.ts.map +0 -1
  213. package/dist/studio-api/routes/projects.js +0 -45
  214. package/dist/studio-api/routes/projects.js.map +0 -1
  215. package/dist/studio-api/routes/registry.d.ts +0 -4
  216. package/dist/studio-api/routes/registry.d.ts.map +0 -1
  217. package/dist/studio-api/routes/registry.js +0 -31
  218. package/dist/studio-api/routes/registry.js.map +0 -1
  219. package/dist/studio-api/routes/render.d.ts +0 -4
  220. package/dist/studio-api/routes/render.d.ts.map +0 -1
  221. package/dist/studio-api/routes/render.js +0 -269
  222. package/dist/studio-api/routes/render.js.map +0 -1
  223. package/dist/studio-api/routes/storyboard.d.ts +0 -4
  224. package/dist/studio-api/routes/storyboard.d.ts.map +0 -1
  225. package/dist/studio-api/routes/storyboard.js +0 -65
  226. package/dist/studio-api/routes/storyboard.js.map +0 -1
  227. package/dist/studio-api/routes/thumbnail.d.ts +0 -4
  228. package/dist/studio-api/routes/thumbnail.d.ts.map +0 -1
  229. package/dist/studio-api/routes/thumbnail.js +0 -107
  230. package/dist/studio-api/routes/thumbnail.js.map +0 -1
  231. package/dist/studio-api/routes/waveform.d.ts +0 -4
  232. package/dist/studio-api/routes/waveform.d.ts.map +0 -1
  233. package/dist/studio-api/routes/waveform.js +0 -41
  234. package/dist/studio-api/routes/waveform.js.map +0 -1
  235. package/dist/studio-api/types.d.ts +0 -120
  236. package/dist/studio-api/types.d.ts.map +0 -1
  237. package/dist/studio-api/types.js +0 -2
  238. package/dist/studio-api/types.js.map +0 -1
@@ -1,1039 +0,0 @@
1
- // Use the acorn read parser: it resolves computed timelines (helpers, bounded
2
- // loops) so lint findings like overlapping_gsap_tweens reflect true positions
3
- // instead of all-collapsed-at-0. It's also browser-safe, so this keeps recast
4
- // out of the lint graph entirely. Dynamic import preserves the lazy load.
5
- async function loadParseGsapScript() {
6
- const mod = await import("../../parsers/gsapParserAcorn.js");
7
- return mod.parseGsapScriptAcorn;
8
- }
9
- import { readAttr, truncateSnippet, stripJsComments, WINDOW_TIMELINE_ASSIGN_PATTERN, } from "../utils.js";
10
- const SCENE_BOUNDARY_EPSILON_SECONDS = 0.05;
11
- // ── GSAP parsing utilities ─────────────────────────────────────────────────
12
- function countClassUsage(tags) {
13
- const counts = new Map();
14
- for (const tag of tags) {
15
- const classAttr = readAttr(tag.raw, "class");
16
- if (!classAttr)
17
- continue;
18
- for (const className of classAttr.split(/\s+/).filter(Boolean)) {
19
- counts.set(className, (counts.get(className) || 0) + 1);
20
- }
21
- }
22
- return counts;
23
- }
24
- function readRegisteredTimelineCompositionId(script) {
25
- const match = script.match(WINDOW_TIMELINE_ASSIGN_PATTERN);
26
- return match?.[1] || match?.[2] || null;
27
- }
28
- /** Strip a `__raw:` prefix the parser adds to unresolvable values. */
29
- function unwrapRaw(value) {
30
- if (typeof value === "number")
31
- return value;
32
- if (typeof value !== "string")
33
- return undefined;
34
- const code = value.startsWith("__raw:") ? value.slice(6) : value;
35
- return code.replace(/^\s*["']|["']\s*$/g, "");
36
- }
37
- function extrasNumber(value) {
38
- const unwrapped = unwrapRaw(value);
39
- const numeric = typeof unwrapped === "number" ? unwrapped : Number(unwrapped);
40
- return Number.isFinite(numeric) ? numeric : 0;
41
- }
42
- /** A readable single-line snippet of a tween for finding messages. */
43
- function synthesizeWindowRaw(timelineVar, anim) {
44
- const entries = Object.entries(anim.properties).map(([k, v]) => {
45
- if (typeof v === "string" && v.startsWith("__raw:"))
46
- return `${k}: ${v.slice(6)}`;
47
- return `${k}: ${typeof v === "string" ? JSON.stringify(v) : v}`;
48
- });
49
- if (anim.duration !== undefined)
50
- entries.push(`duration: ${anim.duration}`);
51
- if (anim.ease)
52
- entries.push(`ease: ${JSON.stringify(anim.ease)}`);
53
- const pos = typeof anim.position === "number" ? anim.position : JSON.stringify(anim.position);
54
- return `${timelineVar}.${anim.method}("${anim.targetSelector}", { ${entries.join(", ")} }, ${pos})`;
55
- }
56
- const gsapWindowsCache = new Map();
57
- async function cachedExtractGsapWindows(scriptContent) {
58
- const cached = gsapWindowsCache.get(scriptContent);
59
- if (cached)
60
- return cached;
61
- const windows = await extractGsapWindows(scriptContent);
62
- gsapWindowsCache.set(scriptContent, windows);
63
- return windows;
64
- }
65
- // fallow-ignore-next-line complexity
66
- async function extractGsapWindows(script) {
67
- if (!/gsap\.timeline/.test(script))
68
- return [];
69
- const parseGsapScript = await loadParseGsapScript();
70
- const parsed = parseGsapScript(script);
71
- if (parsed.animations.length === 0)
72
- return [];
73
- const windows = [];
74
- for (const animation of parsed.animations) {
75
- // Skip animations with string positions (e.g. "+=1", "<") — their absolute
76
- // timing depends on runtime evaluation and can't be statically linted.
77
- if (typeof animation.position !== "number")
78
- continue;
79
- const repeat = extrasNumber(animation.extras?.repeat);
80
- const cycleCount = repeat > 0 ? repeat + 1 : 1;
81
- const effectiveDuration = animation.method === "set" ? 0 : (animation.duration ?? 0) * cycleCount;
82
- windows.push({
83
- targetSelector: animation.targetSelector,
84
- position: animation.position,
85
- end: animation.position + effectiveDuration,
86
- properties: Object.keys(animation.properties),
87
- propertyValues: animation.properties,
88
- overwriteAuto: unwrapRaw(animation.extras?.overwrite) === "auto",
89
- method: animation.method,
90
- raw: synthesizeWindowRaw(parsed.timelineVar, animation),
91
- });
92
- }
93
- return windows;
94
- }
95
- function numberValue(value) {
96
- if (typeof value === "number")
97
- return value;
98
- if (typeof value === "string" && value.trim()) {
99
- const numeric = Number(value);
100
- return Number.isFinite(numeric) ? numeric : null;
101
- }
102
- return null;
103
- }
104
- function stringValue(value) {
105
- if (typeof value === "string")
106
- return value;
107
- if (typeof value === "number")
108
- return String(value);
109
- return null;
110
- }
111
- function zeroValue(value) {
112
- if (typeof value === "number")
113
- return value === 0;
114
- if (typeof value !== "string")
115
- return false;
116
- return Number(value.trim()) === 0;
117
- }
118
- function isHiddenGsapState(values) {
119
- const visibility = stringValue(values.visibility)?.toLowerCase();
120
- const display = stringValue(values.display)?.toLowerCase();
121
- return (zeroValue(values.opacity) ||
122
- zeroValue(values.autoAlpha) ||
123
- visibility === "hidden" ||
124
- display === "none");
125
- }
126
- function oneValue(values, keys) {
127
- for (const key of keys) {
128
- const value = values[key];
129
- if (value !== undefined)
130
- return value;
131
- }
132
- return undefined;
133
- }
134
- function isVisibleGsapState(values) {
135
- const opacity = oneValue(values, ["opacity", "autoAlpha"]);
136
- if (typeof opacity === "number")
137
- return opacity > 0;
138
- if (typeof opacity === "string" && opacity.trim()) {
139
- const numeric = Number(opacity);
140
- if (Number.isFinite(numeric))
141
- return numeric > 0;
142
- }
143
- const visibility = stringValue(values.visibility)?.toLowerCase();
144
- if (visibility === "visible" || visibility === "inherit")
145
- return true;
146
- const display = stringValue(values.display)?.toLowerCase();
147
- if (display && display !== "none")
148
- return true;
149
- return false;
150
- }
151
- function makesOverlayVisible(win) {
152
- if (win.method === "from" && isHiddenGsapState(win.propertyValues))
153
- return true;
154
- return isVisibleGsapState(win.propertyValues);
155
- }
156
- function isSceneBoundaryExit(win) {
157
- if (win.end <= win.position)
158
- return false;
159
- if (win.method !== "to" && win.method !== "fromTo")
160
- return false;
161
- return isHiddenGsapState(win.propertyValues);
162
- }
163
- function isHardKillSet(win, selector, boundary) {
164
- return (win.method === "set" &&
165
- win.targetSelector === selector &&
166
- Math.abs(win.position - boundary) <= SCENE_BOUNDARY_EPSILON_SECONDS &&
167
- isHiddenGsapState(win.propertyValues));
168
- }
169
- function hiddenStateLiteral(values) {
170
- if (zeroValue(values.autoAlpha))
171
- return "{ autoAlpha: 0 }";
172
- if (zeroValue(values.opacity))
173
- return "{ opacity: 0 }";
174
- if (stringValue(values.visibility)?.toLowerCase() === "hidden")
175
- return '{ visibility: "hidden" }';
176
- if (stringValue(values.display)?.toLowerCase() === "none")
177
- return '{ display: "none" }';
178
- return "{ opacity: 0 }";
179
- }
180
- function findTagEnd(source, tag) {
181
- const escapedTagName = tag.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
182
- const pattern = new RegExp(`<\\/?${escapedTagName}\\b[^>]*>`, "gi");
183
- pattern.lastIndex = tag.index;
184
- let depth = 0;
185
- let match;
186
- while ((match = pattern.exec(source)) !== null) {
187
- const raw = match[0];
188
- const isClosing = /^<\s*\//.test(raw);
189
- const isSelfClosing = /\/\s*>$/.test(raw);
190
- if (!isClosing && !isSelfClosing)
191
- depth += 1;
192
- if (isClosing)
193
- depth -= 1;
194
- if (depth === 0)
195
- return pattern.lastIndex;
196
- }
197
- return source.length;
198
- }
199
- function collectCompositionRanges(source, tags) {
200
- return tags
201
- .map((tag) => {
202
- const id = readAttr(tag.raw, "data-composition-id");
203
- if (!id)
204
- return null;
205
- return {
206
- id,
207
- start: tag.index,
208
- end: findTagEnd(source, tag),
209
- };
210
- })
211
- .filter((range) => range !== null);
212
- }
213
- function findContainingCompositionId(tag, ranges) {
214
- let match = null;
215
- for (const range of ranges) {
216
- if (tag.index < range.start || tag.index >= range.end)
217
- continue;
218
- if (!match || range.start >= match.start)
219
- match = range;
220
- }
221
- return match?.id || null;
222
- }
223
- function collectClipStartBoundariesByComposition(source, tags) {
224
- const ranges = collectCompositionRanges(source, tags);
225
- const boundaries = new Map();
226
- for (const tag of tags) {
227
- const classAttr = readAttr(tag.raw, "class") || "";
228
- const classes = classAttr.split(/\s+/).filter(Boolean);
229
- if (!classes.includes("clip"))
230
- continue;
231
- const compositionId = findContainingCompositionId(tag, ranges);
232
- if (!compositionId)
233
- continue;
234
- const start = numberValue(readAttr(tag.raw, "data-start") ?? undefined);
235
- if (start == null || start <= 0)
236
- continue;
237
- const compositionBoundaries = boundaries.get(compositionId) ?? new Set();
238
- compositionBoundaries.add(start);
239
- boundaries.set(compositionId, compositionBoundaries);
240
- }
241
- return new Map([...boundaries.entries()].map(([compositionId, values]) => [
242
- compositionId,
243
- [...values].sort((a, b) => a - b),
244
- ]));
245
- }
246
- function findMatchingSceneBoundary(time, boundaries) {
247
- for (const boundary of boundaries) {
248
- if (Math.abs(time - boundary) <= SCENE_BOUNDARY_EPSILON_SECONDS)
249
- return boundary;
250
- }
251
- return null;
252
- }
253
- function isSuspiciousGlobalSelector(selector) {
254
- if (!selector)
255
- return false;
256
- if (selector.includes("[data-composition-id="))
257
- return false;
258
- if (selector.startsWith("#"))
259
- return false;
260
- return selector.startsWith(".") || /^[a-z]/i.test(selector);
261
- }
262
- function getSingleClassSelector(selector) {
263
- const match = selector.trim().match(/^\.(?<name>[A-Za-z0-9_-]+)$/);
264
- return match?.groups?.name || null;
265
- }
266
- function readStyleProperty(style, property) {
267
- const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
268
- const match = style.match(new RegExp(`(?:^|;)\\s*${escapedProperty}\\s*:\\s*([^;]+)`, "i"));
269
- return match?.[1]?.trim() || null;
270
- }
271
- function cssZero(value) {
272
- if (!value)
273
- return false;
274
- return /^0(?:\.0+)?(?:px|%|vw|vh|rem|em)?$/i.test(value.trim());
275
- }
276
- function styleHasHiddenInitialState(style) {
277
- const opacity = readStyleProperty(style, "opacity");
278
- if (opacity && Number(opacity) === 0)
279
- return true;
280
- if (readStyleProperty(style, "visibility")?.toLowerCase() === "hidden")
281
- return true;
282
- if (readStyleProperty(style, "display")?.toLowerCase() === "none")
283
- return true;
284
- return false;
285
- }
286
- function styleHasOpaqueBackground(style) {
287
- const background = readStyleProperty(style, "background") || readStyleProperty(style, "background-color");
288
- if (!background)
289
- return false;
290
- const normalized = background.toLowerCase().replace(/\s+/g, "");
291
- if (normalized === "transparent" || normalized === "none")
292
- return false;
293
- if (/rgba?\([^)]*,0(?:\.0+)?\)$/.test(normalized))
294
- return false;
295
- if (/hsla?\([^)]*,0(?:\.0+)?\)$/.test(normalized))
296
- return false;
297
- return true;
298
- }
299
- function styleLooksFullFrameOverlay(style) {
300
- const position = readStyleProperty(style, "position")?.toLowerCase();
301
- if (position !== "fixed" && position !== "absolute")
302
- return false;
303
- const coversFrame = cssZero(readStyleProperty(style, "inset")) ||
304
- (cssZero(readStyleProperty(style, "top")) &&
305
- cssZero(readStyleProperty(style, "right")) &&
306
- cssZero(readStyleProperty(style, "bottom")) &&
307
- cssZero(readStyleProperty(style, "left")));
308
- return coversFrame && styleHasOpaqueBackground(style);
309
- }
310
- function collectSimpleStyleRules(styles) {
311
- const rules = new Map();
312
- for (const style of styles) {
313
- for (const [, selectorList, body] of style.content.matchAll(/([^{}]+)\{([^}]+)\}/g)) {
314
- if (!selectorList || !body)
315
- continue;
316
- for (const selector of selectorList.split(",")) {
317
- const token = selector.trim();
318
- if (!/^[#.][A-Za-z0-9_-]+$/.test(token))
319
- continue;
320
- rules.set(token, `${rules.get(token) || ""};${body}`);
321
- }
322
- }
323
- }
324
- return rules;
325
- }
326
- function tagSimpleSelectors(tag) {
327
- const selectors = [];
328
- const id = readAttr(tag.raw, "id");
329
- if (id)
330
- selectors.push(`#${id}`);
331
- const classes = readAttr(tag.raw, "class")?.split(/\s+/).filter(Boolean) ?? [];
332
- for (const className of classes)
333
- selectors.push(`.${className}`);
334
- return selectors;
335
- }
336
- function combinedTagStyle(tag, styleRules) {
337
- const styles = [readAttr(tag.raw, "style") || ""];
338
- for (const selector of tagSimpleSelectors(tag)) {
339
- const ruleStyle = styleRules.get(selector);
340
- if (ruleStyle)
341
- styles.push(ruleStyle);
342
- }
343
- return styles.filter(Boolean).join(";");
344
- }
345
- // fallow-ignore-next-line complexity
346
- function cssTransformToGsapProps(cssTransform) {
347
- const parts = [];
348
- // translate(-50%, -50%) or translate(X, Y)
349
- const translateMatch = cssTransform.match(/translate\(\s*(-?[\d.]+)(%|px)?\s*,\s*(-?[\d.]+)(%|px)?\s*\)/);
350
- if (translateMatch) {
351
- const [, xVal, xUnit, yVal, yUnit] = translateMatch;
352
- if (xUnit === "%")
353
- parts.push(`xPercent: ${xVal}`);
354
- else
355
- parts.push(`x: ${xVal}`);
356
- if (yUnit === "%")
357
- parts.push(`yPercent: ${yVal}`);
358
- else
359
- parts.push(`y: ${yVal}`);
360
- }
361
- // translateX(-50%) or translateX(px)
362
- const txMatch = cssTransform.match(/translateX\(\s*(-?[\d.]+)(%|px)?\s*\)/);
363
- if (txMatch) {
364
- const [, val, unit] = txMatch;
365
- parts.push(unit === "%" ? `xPercent: ${val}` : `x: ${val}`);
366
- }
367
- // translateY(-50%) or translateY(px)
368
- const tyMatch = cssTransform.match(/translateY\(\s*(-?[\d.]+)(%|px)?\s*\)/);
369
- if (tyMatch) {
370
- const [, val, unit] = tyMatch;
371
- parts.push(unit === "%" ? `yPercent: ${val}` : `y: ${val}`);
372
- }
373
- // scale(N)
374
- const scaleMatch = cssTransform.match(/scale\(\s*([\d.]+)\s*\)/);
375
- if (scaleMatch) {
376
- parts.push(`scale: ${scaleMatch[1]}`);
377
- }
378
- return parts.length > 0 ? parts.join(", ") : null;
379
- }
380
- // ── CSS-transform ↔ GSAP-transform conflict matching ─────────────────────────
381
- // Transform components that COMBINE with a CSS translate/scale on the same
382
- // element. GSAP bakes the element's existing CSS transform in when it seeks, so
383
- // these stack rather than override in the capture path (e.g. CSS translateX(-50%)
384
- // + xPercent:-50 renders as -100% — off-centre). `rotation` is excluded: it maps
385
- // to CSS rotate(), which this rule treats separately (no false positive on spin).
386
- const CONFLICTING_TRANSLATE_PROPS = ["x", "y", "xPercent", "yPercent"];
387
- const CONFLICTING_SCALE_PROPS = ["scale", "scaleX", "scaleY"];
388
- // Decompose a (possibly grouped / descendant / compound) GSAP target selector
389
- // into the simple `#id` / `.class` tokens of the elements it actually targets —
390
- // the RIGHTMOST compound of each comma group is the targeted element. This lets a
391
- // CSS rule keyed by a simple selector (`.m04-label`) match a scoped GSAP selector
392
- // (`"#root .m04-label, #root .m04-sub"`), which the prior exact-string lookup
393
- // missed — so every scoped/grouped selector slipped past the rule entirely.
394
- function targetedSelectorTokens(selector) {
395
- const tokens = new Set();
396
- for (const group of selector.split(",")) {
397
- const compounds = group
398
- .trim()
399
- .split(/[\s>+~]+/)
400
- .filter(Boolean);
401
- const last = compounds[compounds.length - 1];
402
- if (!last)
403
- continue;
404
- const simple = last.match(/[#.][A-Za-z0-9_-]+/g);
405
- if (simple)
406
- for (const token of simple)
407
- tokens.add(token);
408
- }
409
- return tokens;
410
- }
411
- // Find a CSS transform conflicting with a GSAP target selector: exact-string
412
- // match first (fast path + back-compat with the original behaviour), then a
413
- // token match so scoped/grouped/descendant selectors resolve to their class/id.
414
- function matchCssTransform(gsapSelector, cssMap) {
415
- if (cssMap.size === 0)
416
- return undefined;
417
- const direct = cssMap.get(gsapSelector);
418
- if (direct)
419
- return direct;
420
- const tokens = targetedSelectorTokens(gsapSelector);
421
- for (const [cssSelector, value] of cssMap) {
422
- if (tokens.has(cssSelector))
423
- return value;
424
- }
425
- return undefined;
426
- }
427
- // Scan for STANDALONE `gsap.set/to/from/fromTo("selector", { ...props })` calls.
428
- // The acorn timeline parser only captures calls rooted on the timeline var
429
- // (`tl.to`, `tl.set`, …); a top-level `gsap.set("#root .label", { xPercent: -50 })`
430
- // — a common way to seat shared base transforms before the timeline runs — is
431
- // invisible to it, so the conflict rule never saw it. Variable selectors
432
- // (`gsap.set(kicker, …)`) can't be resolved statically and are skipped.
433
- function extractStandaloneGsapTransformCalls(script) {
434
- const calls = [];
435
- const pattern = /gsap\.(set|to|from|fromTo)\s*\(\s*(["'])([^"']+)\2\s*,\s*\{([^{}]*)\}/g;
436
- let match;
437
- while ((match = pattern.exec(script)) !== null) {
438
- const method = match[1] ?? "set";
439
- const selector = match[3] ?? "";
440
- const propsBody = match[4] ?? "";
441
- const properties = [...propsBody.matchAll(/([A-Za-z_$][\w$]*)\s*:/g)].map((m) => m[1] ?? "");
442
- calls.push({ method, selector, properties, raw: truncateSnippet(match[0]) ?? match[0] });
443
- }
444
- return calls;
445
- }
446
- // ── GSAP rules ─────────────────────────────────────────────────────────────
447
- // fallow-ignore-next-line complexity
448
- export const gsapRules = [
449
- // overlapping_gsap_tweens + gsap_animates_clip_element + unscoped_gsap_selector
450
- // fallow-ignore-next-line complexity
451
- async ({ source, tags, scripts, styles, rootCompositionId }) => {
452
- const findings = [];
453
- const clipIds = new Map();
454
- const clipClasses = new Map();
455
- for (const tag of tags) {
456
- const classAttr = readAttr(tag.raw, "class") || "";
457
- const classes = classAttr.split(/\s+/).filter(Boolean);
458
- if (!classes.includes("clip"))
459
- continue;
460
- const id = readAttr(tag.raw, "id");
461
- const info = {
462
- tag: tag.name,
463
- id: id || "",
464
- classes: classAttr,
465
- };
466
- if (id)
467
- clipIds.set(`#${id}`, info);
468
- for (const cls of classes) {
469
- if (cls !== "clip")
470
- clipClasses.set(`.${cls}`, info);
471
- }
472
- }
473
- const classUsage = countClassUsage(tags);
474
- const clipStartBoundariesByComposition = collectClipStartBoundariesByComposition(source, tags);
475
- const styleRules = collectSimpleStyleRules(styles);
476
- const reportedVisibleOverlayKeys = new Set();
477
- for (const script of scripts) {
478
- const localTimelineCompId = readRegisteredTimelineCompositionId(script.content);
479
- const gsapWindows = await cachedExtractGsapWindows(script.content);
480
- const clipStartBoundaries = clipStartBoundariesByComposition.get(localTimelineCompId || rootCompositionId || "") ?? [];
481
- // overlapping_gsap_tweens
482
- for (let i = 0; i < gsapWindows.length; i++) {
483
- const left = gsapWindows[i];
484
- if (!left)
485
- continue;
486
- if (left.end <= left.position)
487
- continue;
488
- for (let j = i + 1; j < gsapWindows.length; j++) {
489
- const right = gsapWindows[j];
490
- if (!right)
491
- continue;
492
- if (right.end <= right.position)
493
- continue;
494
- if (left.targetSelector !== right.targetSelector)
495
- continue;
496
- const overlapStart = Math.max(left.position, right.position);
497
- const overlapEnd = Math.min(left.end, right.end);
498
- if (overlapEnd <= overlapStart)
499
- continue;
500
- if (left.overwriteAuto || right.overwriteAuto)
501
- continue;
502
- const sharedProperties = left.properties.filter((prop) => right.properties.includes(prop));
503
- if (sharedProperties.length === 0)
504
- continue;
505
- findings.push({
506
- code: "overlapping_gsap_tweens",
507
- severity: "warning",
508
- message: `GSAP tweens overlap on "${left.targetSelector}" for ${sharedProperties.join(", ")} between ${overlapStart.toFixed(2)}s and ${overlapEnd.toFixed(2)}s.`,
509
- selector: left.targetSelector,
510
- fixHint: 'Shorten the earlier tween, move the later tween, or add `overwrite: "auto"`.',
511
- snippet: truncateSnippet(`${left.raw}\n${right.raw}`),
512
- });
513
- }
514
- }
515
- // gsap_exit_missing_hard_kill
516
- if (clipStartBoundaries.length > 0) {
517
- for (const win of gsapWindows) {
518
- if (!isSceneBoundaryExit(win))
519
- continue;
520
- const boundary = findMatchingSceneBoundary(win.end, clipStartBoundaries);
521
- if (boundary == null)
522
- continue;
523
- const hasHardKill = gsapWindows.some((candidate) => isHardKillSet(candidate, win.targetSelector, boundary));
524
- if (hasHardKill)
525
- continue;
526
- findings.push({
527
- code: "gsap_exit_missing_hard_kill",
528
- severity: "error",
529
- message: `GSAP exit on "${win.targetSelector}" ends at the ${boundary.toFixed(2)}s clip start boundary ` +
530
- "without a matching tl.set hard kill. Non-linear seeking can land after the fade and leave stale visibility state.",
531
- selector: win.targetSelector,
532
- fixHint: `Add \`tl.set("${win.targetSelector}", ${hiddenStateLiteral(win.propertyValues)}, ${boundary.toFixed(2)})\` ` +
533
- "after the exit tween.",
534
- snippet: truncateSnippet(win.raw),
535
- });
536
- }
537
- }
538
- // gsap_fullscreen_overlay_starts_visible
539
- for (const tag of tags) {
540
- const selectors = tagSimpleSelectors(tag);
541
- if (selectors.length === 0)
542
- continue;
543
- const overlayKey = readAttr(tag.raw, "id") || String(tag.index);
544
- if (reportedVisibleOverlayKeys.has(overlayKey))
545
- continue;
546
- const authoredStyle = combinedTagStyle(tag, styleRules);
547
- if (!authoredStyle || !styleLooksFullFrameOverlay(authoredStyle))
548
- continue;
549
- if (styleHasHiddenInitialState(authoredStyle))
550
- continue;
551
- const visibilityWindows = gsapWindows
552
- .filter((win) => {
553
- const tokens = targetedSelectorTokens(win.targetSelector);
554
- if (!selectors.some((selector) => tokens.has(selector)))
555
- return false;
556
- return win.properties.some((prop) => ["opacity", "autoAlpha", "visibility", "display"].includes(prop));
557
- })
558
- .sort((a, b) => a.position - b.position);
559
- const startsHiddenAtZero = visibilityWindows.some((win) => win.position <= SCENE_BOUNDARY_EPSILON_SECONDS && isHiddenGsapState(win.propertyValues));
560
- if (startsHiddenAtZero)
561
- continue;
562
- const firstVisible = visibilityWindows.find((win) => makesOverlayVisible(win));
563
- if (!firstVisible)
564
- continue;
565
- const selector = selectors.find((candidate) => targetedSelectorTokens(firstVisible.targetSelector).has(candidate)) ||
566
- selectors[0] ||
567
- tag.name;
568
- const laterHidden = visibilityWindows.some((win) => win.position >= firstVisible.position && isHiddenGsapState(win.propertyValues));
569
- if (firstVisible.method !== "from" && !laterHidden)
570
- continue;
571
- reportedVisibleOverlayKeys.add(overlayKey);
572
- findings.push({
573
- code: "gsap_fullscreen_overlay_starts_visible",
574
- severity: "error",
575
- message: `Full-frame overlay "${selector}" starts visible before its first GSAP opacity tween at ` +
576
- `${firstVisible.position.toFixed(2)}s. It will cover earlier render frames, often as a blank/white video.`,
577
- selector,
578
- elementId: readAttr(tag.raw, "id") || undefined,
579
- fixHint: `Add \`opacity: 0\` to "${selector}" in CSS/inline styles, or add ` +
580
- `\`tl.set("${selector}", { opacity: 0 }, 0)\` before the reveal tween.`,
581
- snippet: truncateSnippet(firstVisible.raw),
582
- });
583
- }
584
- // gsap_animates_clip_element — only error when GSAP animates visibility/display
585
- for (const win of gsapWindows) {
586
- const sel = win.targetSelector;
587
- const clipInfo = clipIds.get(sel) || clipClasses.get(sel);
588
- if (!clipInfo)
589
- continue;
590
- const conflictingProps = win.properties.filter((p) => p === "visibility" || p === "display");
591
- if (conflictingProps.length === 0)
592
- continue;
593
- const elDesc = `<${clipInfo.tag}${clipInfo.id ? ` id="${clipInfo.id}"` : ""} class="${clipInfo.classes}">`;
594
- findings.push({
595
- code: "gsap_animates_clip_element",
596
- severity: "error",
597
- message: `GSAP animation sets ${conflictingProps.join(", ")} on a clip element. Selector "${sel}" resolves to element ${elDesc}. The framework manages clip visibility via ${conflictingProps.join("/")} — do not animate these properties on clip elements.`,
598
- selector: sel,
599
- elementId: clipInfo.id || undefined,
600
- fixHint: "Remove the visibility/display tween, or move the content into a child <div> and target that instead.",
601
- snippet: truncateSnippet(win.raw),
602
- });
603
- }
604
- // unscoped_gsap_selector
605
- if (!localTimelineCompId || localTimelineCompId === rootCompositionId)
606
- continue;
607
- for (const win of gsapWindows) {
608
- if (!isSuspiciousGlobalSelector(win.targetSelector))
609
- continue;
610
- const className = getSingleClassSelector(win.targetSelector);
611
- if (className && (classUsage.get(className) || 0) < 2)
612
- continue;
613
- findings.push({
614
- code: "unscoped_gsap_selector",
615
- severity: "error",
616
- message: `Timeline "${localTimelineCompId}" uses unscoped selector "${win.targetSelector}" that will target elements in ALL compositions when bundled, causing data loss (opacity, transforms, etc.).`,
617
- selector: win.targetSelector,
618
- fixHint: `Scope the selector: \`[data-composition-id="${localTimelineCompId}"] ${win.targetSelector}\` or use a unique id.`,
619
- snippet: truncateSnippet(win.raw),
620
- });
621
- }
622
- }
623
- return findings;
624
- },
625
- // gsap_css_transform_conflict
626
- // fallow-ignore-next-line complexity
627
- async ({ styles, scripts, tags }) => {
628
- const findings = [];
629
- const cssTranslateSelectors = new Map();
630
- const cssScaleSelectors = new Map();
631
- // Check <style> blocks for transform rules
632
- for (const style of styles) {
633
- for (const [, selector, body] of style.content.matchAll(/([#.][a-zA-Z0-9_-]+)\s*\{([^}]+)\}/g)) {
634
- const tMatch = body?.match(/transform\s*:\s*([^;]+)/);
635
- if (!tMatch || !tMatch[1])
636
- continue;
637
- const transformVal = tMatch[1].trim();
638
- if (/translate/i.test(transformVal))
639
- cssTranslateSelectors.set((selector ?? "").trim(), transformVal);
640
- if (/scale/i.test(transformVal))
641
- cssScaleSelectors.set((selector ?? "").trim(), transformVal);
642
- }
643
- }
644
- // Also check inline style="..." attributes on tags
645
- for (const tag of tags) {
646
- const inlineStyle = readAttr(tag.raw, "style");
647
- if (!inlineStyle)
648
- continue;
649
- const tMatch = inlineStyle.match(/transform\s*:\s*([^;]+)/);
650
- if (!tMatch || !tMatch[1])
651
- continue;
652
- const transformVal = tMatch[1].trim();
653
- // Derive selectors from the tag's id and all classes
654
- const id = readAttr(tag.raw, "id");
655
- const classes = readAttr(tag.raw, "class")?.split(/\s+/).filter(Boolean) ?? [];
656
- const selectors = [];
657
- if (id)
658
- selectors.push(`#${id}`);
659
- for (const cls of classes)
660
- selectors.push(`.${cls}`);
661
- if (selectors.length === 0)
662
- continue;
663
- for (const sel of selectors) {
664
- if (/translate/i.test(transformVal) && !cssTranslateSelectors.has(sel))
665
- cssTranslateSelectors.set(sel, transformVal);
666
- if (/scale/i.test(transformVal) && !cssScaleSelectors.has(sel))
667
- cssScaleSelectors.set(sel, transformVal);
668
- }
669
- }
670
- if (cssTranslateSelectors.size === 0 && cssScaleSelectors.size === 0)
671
- return findings;
672
- for (const script of scripts) {
673
- if (!/gsap\.timeline/.test(script.content))
674
- continue;
675
- const windows = await cachedExtractGsapWindows(script.content);
676
- // Two sources of transform-setting calls: timeline-rooted tweens (from the
677
- // acorn parser) and standalone gsap.* calls (regex — the parser ignores
678
- // these). Normalize both into one shape and run the same conflict check.
679
- const calls = [
680
- ...windows.map((win) => ({
681
- method: win.method,
682
- selector: win.targetSelector,
683
- properties: win.properties,
684
- raw: win.raw,
685
- })),
686
- ...extractStandaloneGsapTransformCalls(stripJsComments(script.content)),
687
- ];
688
- const conflicts = new Map();
689
- for (const call of calls) {
690
- // from() and fromTo() both supply explicit start values so GSAP owns
691
- // the full transform from t=0, making the CSS conflict moot
692
- if (call.method === "fromTo" || call.method === "from")
693
- continue;
694
- const sel = call.selector;
695
- const translateProps = call.properties.filter((p) => CONFLICTING_TRANSLATE_PROPS.includes(p));
696
- const scaleProps = call.properties.filter((p) => CONFLICTING_SCALE_PROPS.includes(p));
697
- const cssFromTranslate = translateProps.length > 0 ? matchCssTransform(sel, cssTranslateSelectors) : undefined;
698
- const cssFromScale = scaleProps.length > 0 ? matchCssTransform(sel, cssScaleSelectors) : undefined;
699
- if (!cssFromTranslate && !cssFromScale)
700
- continue;
701
- const existing = conflicts.get(sel) ?? {
702
- cssTransform: [cssFromTranslate, cssFromScale].filter(Boolean).join(" "),
703
- props: new Set(),
704
- raw: call.raw,
705
- };
706
- for (const p of [...translateProps, ...scaleProps])
707
- existing.props.add(p);
708
- conflicts.set(sel, existing);
709
- }
710
- for (const [sel, { cssTransform, props, raw }] of conflicts) {
711
- const propList = [...props].join("/");
712
- const gsapEquivalent = cssTransformToGsapProps(cssTransform);
713
- const fixHint = gsapEquivalent
714
- ? `Remove \`transform: ${cssTransform}\` from CSS and replace with GSAP properties: ${gsapEquivalent}. ` +
715
- `Example: tl.fromTo('${sel}', { ${gsapEquivalent} }, { ${gsapEquivalent}, ...yourAnimation }). ` +
716
- `tl.fromTo is exempt from this rule.`
717
- : `Remove the transform from CSS and use tl.fromTo('${sel}', ` +
718
- `{ xPercent: -50, x: -1000 }, { xPercent: -50, x: 0 }) so GSAP owns ` +
719
- `the full transform state. tl.fromTo is exempt from this rule.`;
720
- findings.push({
721
- code: "gsap_css_transform_conflict",
722
- severity: "error",
723
- message: `"${sel}" has CSS \`transform: ${cssTransform}\` and a GSAP tween animates ` +
724
- `${propList}. GSAP will overwrite the full CSS transform, discarding any ` +
725
- `translateX(-50%) centering or CSS scale value.`,
726
- selector: sel,
727
- fixHint,
728
- snippet: truncateSnippet(raw),
729
- });
730
- }
731
- }
732
- return findings;
733
- },
734
- // missing_gsap_script
735
- ({ scripts, rawSource, options }) => {
736
- const allScriptTexts = scripts.filter((s) => !/\bsrc\s*=/.test(s.attrs)).map((s) => s.content);
737
- const allScriptSrcs = scripts
738
- .map((s) => readAttr(`<script ${s.attrs}>`, "src") || "")
739
- .filter(Boolean);
740
- const canInheritGsapFromHost = options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template");
741
- const usesGsap = allScriptTexts.some((t) => /gsap\.(to|from|fromTo|timeline|set|registerPlugin)\b/.test(t));
742
- const hasGsapScript = allScriptSrcs.some((src) => /gsap/i.test(src));
743
- // Detect GSAP bundled inline (no src attribute). Match:
744
- // - Producer's CDN-inlining comment: /* inlined: ...gsap... */
745
- // - GSAP library internals: _gsScope, GreenSock, gsap.config
746
- // - Large inline scripts (>5KB) that reference gsap (likely bundled library)
747
- const hasInlineGsap = allScriptTexts.some((t) => /\/\*\s*inlined:.*gsap/i.test(t) ||
748
- /\b_gsScope\b/.test(t) ||
749
- /\bGreenSock\b/.test(t) ||
750
- /\bgsap\.(config|defaults|version)\b/.test(t) ||
751
- (t.length > 5000 && /\bgsap\b/i.test(t)));
752
- if (!usesGsap || hasGsapScript || hasInlineGsap || canInheritGsapFromHost)
753
- return [];
754
- return [
755
- {
756
- code: "missing_gsap_script",
757
- severity: "error",
758
- message: "Composition uses GSAP but no GSAP script is loaded. The animation will not run.",
759
- fixHint: 'Add <script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script> before your animation script.',
760
- },
761
- ];
762
- },
763
- // audio_reactive_single_tween_per_group
764
- // fallow-ignore-next-line complexity
765
- ({ scripts, styles }) => {
766
- const findings = [];
767
- const isCaptionFile = styles.some((s) => /\.caption[-_]?(?:group|word)/i.test(s.content));
768
- if (!isCaptionFile)
769
- return findings;
770
- for (const script of scripts) {
771
- const content = script.content;
772
- // Detect audio data loading
773
- const hasAudioData = /AUDIO|audio[-_]?data|bands\[/.test(content);
774
- if (!hasAudioData)
775
- continue;
776
- // Detect caption group loop
777
- const hasCaptionLoop = /forEach/.test(content) && /caption|group|cg-/.test(content);
778
- if (!hasCaptionLoop)
779
- continue;
780
- // Check if audio-reactive tweens are created at intervals (loop inside the group loop)
781
- // vs a single tween per group (no inner time-sampling loop)
782
- const hasInnerSamplingLoop = /for\s*\(\s*var\s+\w+\s*=\s*group\.start/.test(content) ||
783
- /for\s*\(\s*var\s+at\s*=/.test(content) ||
784
- /while\s*\(\s*\w+\s*<\s*group\.end/.test(content);
785
- if (!hasInnerSamplingLoop) {
786
- // Check if there's at least a peak-based single tween (the minimal pattern)
787
- const hasPeakTween = /peak(?:Bass|Treble|Energy)/.test(content) && /group\.start/.test(content);
788
- if (hasPeakTween) {
789
- findings.push({
790
- code: "audio_reactive_single_tween_per_group",
791
- severity: "warning",
792
- message: "Audio-reactive captions use a single tween per group based on peak values. " +
793
- "This sets one static value at group.start — not perceptible as audio reactivity.",
794
- fixHint: "Sample audio data at 100-200ms intervals throughout each group's lifetime " +
795
- "(for loop from group.start to group.end) and create a tween at each sample " +
796
- "point for visible pulsing.",
797
- });
798
- }
799
- }
800
- }
801
- return findings;
802
- },
803
- // gsap_infinite_repeat
804
- ({ scripts }) => {
805
- const findings = [];
806
- for (const script of scripts) {
807
- const content = stripJsComments(script.content);
808
- // Match repeat: -1 in GSAP tweens or timeline configs
809
- const pattern = /repeat\s*:\s*-1(?!\d)/g;
810
- let match;
811
- while ((match = pattern.exec(content)) !== null) {
812
- const contextStart = Math.max(0, match.index - 60);
813
- const contextEnd = Math.min(content.length, match.index + match[0].length + 60);
814
- const snippet = content.slice(contextStart, contextEnd).trim();
815
- findings.push({
816
- code: "gsap_infinite_repeat",
817
- severity: "error",
818
- message: "GSAP tween uses `repeat: -1` (infinite). Infinite repeats break the deterministic " +
819
- "capture engine which seeks to exact frame times. Use a finite repeat count calculated " +
820
- "from the composition duration: `repeat: Math.floor(duration / cycleDuration) - 1`.",
821
- fixHint: "Replace `repeat: -1` with a finite count, e.g. `repeat: Math.floor(totalDuration / singleCycleDuration) - 1`. " +
822
- "Use Math.floor (not Math.ceil) to ensure the animation fits within the total duration.",
823
- snippet: truncateSnippet(snippet),
824
- });
825
- }
826
- }
827
- return findings;
828
- },
829
- // gsap_repeat_ceil_overshoot
830
- ({ scripts }) => {
831
- const findings = [];
832
- for (const script of scripts) {
833
- const content = script.content;
834
- // Match patterns like: repeat: Math.ceil(duration / X) - 1
835
- // or repeat: Math.ceil(totalDuration / cycleDuration) - 1
836
- const pattern = /repeat\s*:\s*Math\.ceil\s*\([^)]+\)\s*-\s*1/g;
837
- let match;
838
- while ((match = pattern.exec(content)) !== null) {
839
- const contextStart = Math.max(0, match.index - 40);
840
- const contextEnd = Math.min(content.length, match.index + match[0].length + 40);
841
- const snippet = content.slice(contextStart, contextEnd).trim();
842
- findings.push({
843
- code: "gsap_repeat_ceil_overshoot",
844
- severity: "warning",
845
- message: "GSAP repeat calculation uses `Math.ceil` which can overshoot the composition duration. " +
846
- "For example, Math.ceil(10.5 / 2) - 1 = 5 repeats → 6 cycles × 2s = 12s, exceeding 10.5s.",
847
- fixHint: "Use `Math.floor` instead of `Math.ceil` to ensure the animation fits within the duration: " +
848
- "`repeat: Math.floor(totalDuration / cycleDuration) - 1`. " +
849
- "Math.floor(10.5 / 2) - 1 = 4 repeats → 5 cycles × 2s = 10s ✓",
850
- snippet: truncateSnippet(snippet),
851
- });
852
- }
853
- }
854
- return findings;
855
- },
856
- // scene_layer_missing_visibility_kill
857
- ({ scripts, tags }) => {
858
- const findings = [];
859
- // Detect multi-scene compositions: multiple elements with "scene" in their id
860
- const sceneElements = tags.filter((t) => {
861
- const id = readAttr(t.raw, "id") || "";
862
- return /^scene\d+$/i.test(id);
863
- });
864
- if (sceneElements.length < 2)
865
- return findings;
866
- for (const script of scripts) {
867
- const content = stripJsComments(script.content);
868
- // For each scene, check if there's a visibility:hidden set after exit tweens
869
- for (const tag of sceneElements) {
870
- const id = readAttr(tag.raw, "id") || "";
871
- // Check if this scene has exit tweens (opacity: 0)
872
- const exitPattern = new RegExp(`["']#${id}["'][^)]*opacity\\s*:\\s*0`);
873
- const hasExit = exitPattern.test(content);
874
- if (!hasExit)
875
- continue;
876
- // Check if there's a hard visibility kill
877
- const killPattern = new RegExp(`["']#${id}["'][^)]*visibility\\s*:\\s*["']hidden["']`);
878
- const hasKill = killPattern.test(content);
879
- if (!hasKill) {
880
- findings.push({
881
- code: "scene_layer_missing_visibility_kill",
882
- severity: "error",
883
- elementId: id,
884
- message: `Scene layer "#${id}" exits via opacity tween but has no visibility: hidden hard kill. ` +
885
- "When scrubbing or when tweens conflict, the scene may remain partially visible and overlap the next scene.",
886
- fixHint: `Add \`tl.set("#${id}", { visibility: "hidden" }, <exit-end-time>)\` after the scene's exit tweens.`,
887
- });
888
- }
889
- }
890
- }
891
- return findings;
892
- },
893
- // gsap_timeline_not_registered
894
- ({ scripts, rawSource, options }) => {
895
- const findings = [];
896
- const canInheritFromHost = options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template");
897
- for (const script of scripts) {
898
- const content = script.content;
899
- if (!/gsap\.timeline/.test(content))
900
- continue;
901
- const hasRegistration = WINDOW_TIMELINE_ASSIGN_PATTERN.test(content);
902
- if (hasRegistration || canInheritFromHost)
903
- continue;
904
- findings.push({
905
- code: "gsap_timeline_not_registered",
906
- severity: "error",
907
- message: "GSAP timeline is created but never registered in window.__timelines. " +
908
- "The runtime discovers timelines from this registry — without registration, " +
909
- "animations will not play during preview or render.",
910
- fixHint: "Add `window.__timelines = window.__timelines || {};` and " +
911
- '`window.__timelines["root"] = tl;` after creating the timeline (use the ' +
912
- "composition's data-composition-id as the key).",
913
- });
914
- }
915
- return findings;
916
- },
917
- // gsap_timeline_registered_before_async_build — registering window.__timelines[id]
918
- // BEFORE the timeline is built inside document.fonts.ready (or any async callback)
919
- // leaves an EMPTY timeline registered. The runtime's sub-composition readiness gate
920
- // treats "key present" as "ready" and nests the child ONCE, while still empty — so the
921
- // animation never renders when this composition is mounted as a sub-composition.
922
- // Register only AFTER the build completes (the documented async-setup contract).
923
- ({ scripts }) => {
924
- const findings = [];
925
- for (const script of scripts) {
926
- const content = stripJsComments(script.content);
927
- const regIdx = content.search(/window\s*\.\s*__timelines\s*\[/);
928
- if (regIdx < 0)
929
- continue;
930
- const fontsReadyIdx = content.search(/document\s*\.\s*fonts\s*\.\s*ready/);
931
- if (fontsReadyIdx < 0)
932
- continue;
933
- // Registering after the async boundary is the correct pattern — skip it.
934
- if (regIdx >= fontsReadyIdx)
935
- continue;
936
- // Confirm the build is actually deferred past the boundary (a tween/build call
937
- // appears after document.fonts.ready), i.e. the registered timeline starts empty.
938
- const tail = content.slice(fontsReadyIdx);
939
- if (!/\.(?:to|from|fromTo)\s*\(|buildEffect\s*\(/.test(tail))
940
- continue;
941
- findings.push({
942
- code: "gsap_timeline_registered_before_async_build",
943
- severity: "error",
944
- message: "window.__timelines is assigned BEFORE the timeline is built inside " +
945
- "document.fonts.ready. An empty timeline registered early gets nested empty " +
946
- "when this composition is used as a sub-composition (the readiness gate treats " +
947
- '"key present" as "ready" and never re-nests), so the animation renders blank.',
948
- fixHint: "Move the `window.__timelines[id] = tl;` assignment to the END of the " +
949
- "document.fonts.ready callback, after the tweens are added. Optionally call " +
950
- "window.__hfForceTimelineRebind() right after, to re-nest the populated timeline.",
951
- });
952
- }
953
- return findings;
954
- },
955
- // gsap_from_opacity_noop — CSS opacity:0 + gsap.from({opacity:0}) = invisible forever
956
- // fallow-ignore-next-line complexity
957
- async ({ styles, scripts, tags }) => {
958
- const findings = [];
959
- const cssOpacityZeroSelectors = new Set();
960
- for (const style of styles) {
961
- for (const [, selector, body] of style.content.matchAll(/([#.][a-zA-Z0-9_-]+)\s*\{([^}]+)\}/g)) {
962
- if (body && /opacity\s*:\s*0\s*[;}]/.test(body)) {
963
- cssOpacityZeroSelectors.add((selector ?? "").trim());
964
- }
965
- }
966
- }
967
- for (const tag of tags) {
968
- const inlineStyle = readAttr(tag.raw, "style");
969
- if (!inlineStyle || !/opacity\s*:\s*0/.test(inlineStyle))
970
- continue;
971
- const id = readAttr(tag.raw, "id");
972
- const classes = readAttr(tag.raw, "class")?.split(/\s+/).filter(Boolean) ?? [];
973
- if (id)
974
- cssOpacityZeroSelectors.add(`#${id}`);
975
- for (const cls of classes)
976
- cssOpacityZeroSelectors.add(`.${cls}`);
977
- }
978
- if (cssOpacityZeroSelectors.size === 0)
979
- return findings;
980
- for (const script of scripts) {
981
- if (!/gsap\.timeline/.test(script.content))
982
- continue;
983
- const windows = await cachedExtractGsapWindows(script.content);
984
- for (const win of windows) {
985
- if (win.method !== "from")
986
- continue;
987
- if (!win.properties.includes("opacity"))
988
- continue;
989
- // Only a noop when the tween animates FROM 0 (same as the CSS value)
990
- if (win.propertyValues["opacity"] !== 0)
991
- continue;
992
- const sel = win.targetSelector;
993
- const cssKey = sel.startsWith("#") || sel.startsWith(".") ? sel : `#${sel}`;
994
- if (!cssOpacityZeroSelectors.has(cssKey))
995
- continue;
996
- findings.push({
997
- code: "gsap_from_opacity_noop",
998
- severity: "error",
999
- message: `"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. ` +
1000
- `gsap.from() animates FROM the specified value TO the current CSS value — ` +
1001
- `since CSS is already 0, the element animates from 0→0 and never becomes visible.`,
1002
- selector: sel,
1003
- fixHint: `Remove \`opacity: 0\` from the CSS/inline style on "${sel}". ` +
1004
- `Let gsap.from({opacity: 0}) handle the initial hidden state — ` +
1005
- `it will animate FROM 0 TO the CSS value (1 by default).`,
1006
- snippet: truncateSnippet(win.raw),
1007
- });
1008
- }
1009
- }
1010
- return findings;
1011
- },
1012
- // gsap_group_selector_keyframes
1013
- ({ scripts }) => {
1014
- const findings = [];
1015
- for (const script of scripts) {
1016
- const content = stripJsComments(script.content);
1017
- const pattern = /\.(?:to|from|fromTo)\(\s*["']([^"']+,\s*[^"']+)["']\s*,\s*\{[^}]*keyframes/g;
1018
- let match;
1019
- while ((match = pattern.exec(content)) !== null) {
1020
- const selector = match[1];
1021
- const count = selector.split(",").length;
1022
- const contextStart = Math.max(0, match.index - 20);
1023
- const contextEnd = Math.min(content.length, match.index + match[0].length + 40);
1024
- findings.push({
1025
- code: "gsap_group_selector_keyframes",
1026
- severity: "warning",
1027
- message: `GSAP tween targets ${count} elements with shared keyframes ("${truncateSnippet(selector, 60)}"). ` +
1028
- `Editing one element's keyframes in Studio will affect all ${count} elements. ` +
1029
- `Split into individual tweens for per-element keyframe control.`,
1030
- fixHint: `Replace the group selector with individual tl.to() calls per element, ` +
1031
- `each with their own keyframes object.`,
1032
- snippet: truncateSnippet(content.slice(contextStart, contextEnd)),
1033
- });
1034
- }
1035
- }
1036
- return findings;
1037
- },
1038
- ];
1039
- //# sourceMappingURL=gsap.js.map