@@ -145,6 +145,47 @@ export function migrate(source) {
145
145
}
146
146
}
147
147
148
+ /**
149
+ * If true, then we need to move all reactive statements to the end of the script block,
150
+ * in their correct order. Svelte 4 reordered reactive statements, $derived/$effect.pre
151
+ * don't have this behavior.
152
+ */
153
+ let needs_reordering = false ;
154
+
155
+ for ( const [ node , { dependencies } ] of state . analysis . reactive_statements ) {
156
+ /** @type {Compiler.Binding[] } */
157
+ let ids = [ ] ;
158
+ if (
159
+ node . body . type === 'ExpressionStatement' &&
160
+ node . body . expression . type === 'AssignmentExpression'
161
+ ) {
162
+ ids = extract_identifiers ( node . body . expression . left )
163
+ . map ( ( id ) => state . scope . get ( id . name ) )
164
+ . filter ( ( id ) => ! ! id ) ;
165
+ }
166
+
167
+ if (
168
+ dependencies . some (
169
+ ( dep ) =>
170
+ ! ids . includes ( dep ) &&
171
+ /** @type {number } */ ( dep . node . start ) > /** @type {number } */ ( node . start )
172
+ )
173
+ ) {
174
+ needs_reordering = true ;
175
+ break ;
176
+ }
177
+ }
178
+
179
+ if ( needs_reordering ) {
180
+ const nodes = Array . from ( state . analysis . reactive_statements . keys ( ) ) ;
181
+ for ( const node of nodes ) {
182
+ const { start, end } = get_node_range ( source , node ) ;
183
+ str . appendLeft ( end , '\n' ) ;
184
+ str . move ( start , end , /** @type {number } */ ( parsed . instance ?. content . end ) ) ;
185
+ str . remove ( start - ( source [ start - 2 ] === '\r' ? 2 : 1 ) , start ) ;
186
+ }
187
+ }
188
+
148
189
if ( state . needs_run && ! added_legacy_import ) {
149
190
if ( parsed . instance ) {
150
191
str . appendRight (
@@ -377,7 +418,7 @@ const instance_script = {
377
418
/** @type {number } */ ( node . body . expression . start ) ,
378
419
'let '
379
420
) ;
380
- state . str . prependLeft (
421
+ state . str . prependRight (
381
422
/** @type {number } */ ( node . body . expression . right . start ) ,
382
423
'$derived('
383
424
) ;
@@ -388,14 +429,14 @@ const instance_script = {
388
429
');'
389
430
) ;
390
431
} else {
391
- state . str . appendRight ( /** @type {number } */ ( node . end ) , ');' ) ;
432
+ state . str . appendLeft ( /** @type {number } */ ( node . end ) , ');' ) ;
392
433
}
393
434
return ;
394
435
} else {
395
436
for ( const binding of reassigned_bindings ) {
396
437
if ( binding && ids . includes ( binding . node ) ) {
397
438
// implicitly-declared variable which we need to make explicit
398
- state . str . prependLeft (
439
+ state . str . prependRight (
399
440
/** @type {number } */ ( node . start ) ,
400
441
`let ${ binding . node . name } ${ binding . kind === 'state' ? ' = $state()' : '' } ;\n${ state . indent } `
401
442
) ;
@@ -428,7 +469,7 @@ const instance_script = {
428
469
[ /** @type {number } */ ( node . body . end ) , state . end ]
429
470
]
430
471
} ) ;
431
- state . str . appendRight ( /** @type {number } */ ( node . end ) , `\n${ state . indent } });` ) ;
472
+ state . str . appendLeft ( /** @type {number } */ ( node . end ) , `\n${ state . indent } });` ) ;
432
473
}
433
474
}
434
475
} ;
@@ -808,6 +849,30 @@ function get_indent(state, ...nodes) {
808
849
return indent ;
809
850
}
810
851
852
+ /**
853
+ * Returns start and end of the node. If the start is preceeded with white-space-only before a line break,
854
+ * the start will be the start of the line.
855
+ * @param {string } source
856
+ * @param {Node } node
857
+ */
858
+ function get_node_range ( source , node ) {
859
+ let start = /** @type {number } */ ( node . start ) ;
860
+ let end = /** @type {number } */ ( node . end ) ;
861
+
862
+ let idx = start ;
863
+ while ( source [ idx - 1 ] !== '\n' && source [ idx - 1 ] !== '\r' ) {
864
+ idx -- ;
865
+ if ( source [ idx ] !== ' ' && source [ idx ] !== '\t' ) {
866
+ idx = start ;
867
+ break ;
868
+ }
869
+ }
870
+
871
+ start = idx ;
872
+
873
+ return { start, end } ;
874
+ }
875
+
811
876
/**
812
877
* @param {Compiler.OnDirective } last
813
878
* @param {State } state
0 commit comments