1- // fallow-ignore-file duplication
1+ // fallow-ignore-file code- duplication
22/**
33 * Browser-safe GSAP write path — magic-string offset-splice.
44 *
88 */
99import MagicString from "magic-string" ;
1010import type { GsapAnimation } from "./gsapSerialize.js" ;
11- import { parseGsapScriptAcornForWrite , type TweenCallInfo } from "./gsapParserAcorn.js" ;
11+ import {
12+ parseGsapScriptAcornForWrite ,
13+ type ParsedGsapAcornForWrite ,
14+ type TweenCallInfo ,
15+ } from "./gsapParserAcorn.js" ;
1216import * as acornWalk from "acorn-walk" ;
1317
1418// ── Code generation helpers ──────────────────────────────────────────────────
1519
16- function valueToCode ( value : number | string ) : string {
20+ function valueToCode ( value : unknown ) : string {
1721 if ( typeof value === "string" && value . startsWith ( "__raw:" ) ) return value . slice ( 6 ) ;
1822 if ( typeof value === "string" ) return JSON . stringify ( value ) ;
19- return String ( value ) ;
23+ if ( typeof value === "number" || typeof value === "boolean" ) return String ( value ) ;
24+ return JSON . stringify ( value ) ;
2025}
2126
2227function safeKey ( key : string ) : string {
@@ -32,7 +37,7 @@ function buildTweenStatementCode(timelineVar: string, anim: Omit<GsapAnimation,
3237 const entries = Object . entries ( props ) . map ( ( [ k , v ] ) => `${ safeKey ( k ) } : ${ valueToCode ( v ) } ` ) ;
3338 if ( anim . extras ) {
3439 for ( const [ k , v ] of Object . entries ( anim . extras ) ) {
35- entries . push ( `${ safeKey ( k ) } : ${ valueToCode ( v as number | string ) } ` ) ;
40+ entries . push ( `${ safeKey ( k ) } : ${ valueToCode ( v ) } ` ) ;
3641 }
3742 }
3843 const objCode = `{ ${ entries . join ( ", " ) } }` ;
@@ -121,7 +126,7 @@ function removeProp(ms: MagicString, propNode: any, editableProps: any[]): void
121126 * Update a property value if it exists, or append a new key: val before the
122127 * closing `}`. Call with the full ObjectExpression node.
123128 */
124- function upsertProp ( ms : MagicString , objNode : any , key : string , value : number | string ) : void {
129+ function upsertProp ( ms : MagicString , objNode : any , key : string , value : unknown ) : void {
125130 if ( objNode ?. type !== "ObjectExpression" ) return ;
126131 const existing = findPropertyNode ( objNode , key ) ;
127132 if ( existing ) {
@@ -132,6 +137,31 @@ function upsertProp(ms: MagicString, objNode: any, key: string, value: number |
132137 }
133138}
134139
140+ // ── Insertion helpers ─────────────────────────────────────────────────────────
141+
142+ /** Traverse callee.object chain to check if a call ultimately roots at timelineVar. */
143+ function isTimelineRooted ( node : any , timelineVar : string ) : boolean {
144+ if ( node ?. type === "Identifier" ) return node . name === timelineVar ;
145+ if ( node ?. type === "CallExpression" ) return isTimelineRooted ( node . callee ?. object , timelineVar ) ;
146+ return false ;
147+ }
148+
149+ /**
150+ * Find the byte offset after which to insert a new statement (tween or label).
151+ * Returns null when no timeline declaration exists in the script — callers must
152+ * not emit `tl.xxx()` calls in that case as `tl` would be undefined at render.
153+ */
154+ function findInsertionPoint ( parsed : ParsedGsapAcornForWrite ) : number | null {
155+ if ( parsed . located . length > 0 ) {
156+ const lastCall = parsed . located [ parsed . located . length - 1 ] ! . call ;
157+ const exprStmt = findEnclosingExpressionStatement ( lastCall . ancestors ) ;
158+ return exprStmt ?. end ?? lastCall . node . end ;
159+ }
160+ if ( ! parsed . hasTimeline ) return null ;
161+ const tlDecl = findTimelineDeclarationStatement ( parsed . ast , parsed . timelineVar ) ;
162+ return tlDecl ?. end ?? ( parsed . ast . end as number ) ;
163+ }
164+
135165// ── Public write API ─────────────────────────────────────────────────────────
136166
137167// fallow-ignore-next-line complexity
@@ -179,6 +209,12 @@ export function updateAnimationInScript(
179209 }
180210 }
181211
212+ if ( updates . extras ) {
213+ for ( const [ key , value ] of Object . entries ( updates . extras ) ) {
214+ upsertProp ( ms , call . varsArg , key , value ) ;
215+ }
216+ }
217+
182218 return ms . toString ( ) ;
183219}
184220
@@ -189,19 +225,11 @@ export function addAnimationToScript(
189225 const parsed = parseGsapScriptAcornForWrite ( script ) ;
190226 if ( ! parsed ) return { script, id : "" } ;
191227
228+ const insertionPoint = findInsertionPoint ( parsed ) ;
229+ if ( insertionPoint === null ) return { script, id : "" } ;
230+
192231 const ms = new MagicString ( script ) ;
193232 const statementCode = buildTweenStatementCode ( parsed . timelineVar , animation ) ;
194-
195- let insertionPoint : number ;
196- if ( parsed . located . length > 0 ) {
197- const lastCall = parsed . located [ parsed . located . length - 1 ] ! . call ;
198- const exprStmt = findEnclosingExpressionStatement ( lastCall . ancestors ) ;
199- insertionPoint = exprStmt ?. end ?? lastCall . node . end ;
200- } else {
201- const tlDecl = findTimelineDeclarationStatement ( parsed . ast , parsed . timelineVar ) ;
202- insertionPoint = tlDecl ?. end ?? script . length ;
203- }
204-
205233 ms . appendLeft ( insertionPoint , "\n" + statementCode ) ;
206234
207235 const result = ms . toString ( ) ;
@@ -366,3 +394,51 @@ export function removeKeyframeFromScript(
366394 removeProp ( ms , match . prop , allProps ) ;
367395 return ms . toString ( ) ;
368396}
397+
398+ // ── Label write ops ───────────────────────────────────────────────────────────
399+
400+ export function addLabelToScript ( script : string , name : string , position : number ) : string {
401+ const parsed = parseGsapScriptAcornForWrite ( script ) ;
402+ if ( ! parsed ) return script ;
403+
404+ const insertionPoint = findInsertionPoint ( parsed ) ;
405+ if ( insertionPoint === null ) return script ;
406+
407+ const ms = new MagicString ( script ) ;
408+ const labelCode = `${ parsed . timelineVar } .addLabel(${ JSON . stringify ( name ) } , ${ valueToCode ( position ) } );` ;
409+ ms . appendLeft ( insertionPoint , "\n" + labelCode ) ;
410+ return ms . toString ( ) ;
411+ }
412+
413+ export function removeLabelFromScript ( script : string , name : string ) : string {
414+ const parsed = parseGsapScriptAcornForWrite ( script ) ;
415+ if ( ! parsed ) return script ;
416+
417+ const targets : any [ ] = [ ] ;
418+ acornWalk . simple ( parsed . ast , {
419+ // fallow-ignore-next-line complexity
420+ ExpressionStatement ( node : any ) {
421+ const expr = node . expression ;
422+ if (
423+ expr ?. type === "CallExpression" &&
424+ expr . callee ?. type === "MemberExpression" &&
425+ isTimelineRooted ( expr . callee . object , parsed . timelineVar ) &&
426+ expr . callee . property ?. name === "addLabel" &&
427+ expr . arguments ?. [ 0 ] ?. type === "Literal" &&
428+ expr . arguments [ 0 ] . value === name
429+ ) {
430+ targets . push ( node ) ;
431+ }
432+ } ,
433+ } ) ;
434+
435+ if ( ! targets . length ) return script ;
436+
437+ const ms = new MagicString ( script ) ;
438+ for ( const target of targets ) {
439+ const end =
440+ target . end < script . length && script [ target . end ] === "\n" ? target . end + 1 : target . end ;
441+ ms . remove ( target . start , end ) ;
442+ }
443+ return ms . toString ( ) ;
444+ }
0 commit comments