Skip to content

Commit 2ffb55c

Browse files
authored
fix: reorder reactive statements during migration (#12329)
fixes #12183
1 parent bbf4895 commit 2ffb55c

File tree

6 files changed

+106
-4
lines changed

6 files changed

+106
-4
lines changed

.changeset/few-cheetahs-taste.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: reorder reactive statements during migration

packages/svelte/src/compiler/migrate/index.js

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,47 @@ export function migrate(source) {
145145
}
146146
}
147147

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+
148189
if (state.needs_run && !added_legacy_import) {
149190
if (parsed.instance) {
150191
str.appendRight(
@@ -377,7 +418,7 @@ const instance_script = {
377418
/** @type {number} */ (node.body.expression.start),
378419
'let '
379420
);
380-
state.str.prependLeft(
421+
state.str.prependRight(
381422
/** @type {number} */ (node.body.expression.right.start),
382423
'$derived('
383424
);
@@ -388,14 +429,14 @@ const instance_script = {
388429
');'
389430
);
390431
} else {
391-
state.str.appendRight(/** @type {number} */ (node.end), ');');
432+
state.str.appendLeft(/** @type {number} */ (node.end), ');');
392433
}
393434
return;
394435
} else {
395436
for (const binding of reassigned_bindings) {
396437
if (binding && ids.includes(binding.node)) {
397438
// implicitly-declared variable which we need to make explicit
398-
state.str.prependLeft(
439+
state.str.prependRight(
399440
/** @type {number} */ (node.start),
400441
`let ${binding.node.name}${binding.kind === 'state' ? ' = $state()' : ''};\n${state.indent}`
401442
);
@@ -428,7 +469,7 @@ const instance_script = {
428469
[/** @type {number} */ (node.body.end), state.end]
429470
]
430471
});
431-
state.str.appendRight(/** @type {number} */ (node.end), `\n${state.indent}});`);
472+
state.str.appendLeft(/** @type {number} */ (node.end), `\n${state.indent}});`);
432473
}
433474
}
434475
};
@@ -808,6 +849,30 @@ function get_indent(state, ...nodes) {
808849
return indent;
809850
}
810851

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+
811876
/**
812877
* @param {Compiler.OnDirective} last
813878
* @param {State} state
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
$: mobile = width < 640;
3+
$: x = !mobile;
4+
let width = 0;
5+
</script>
6+
7+
{width / mobile / x}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let width = 0;
3+
let mobile = $derived(width < 640);
4+
let x = $derived(!mobile);
5+
</script>
6+
7+
{width / mobile / x}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let width = 0;
3+
$: console.log(mobile);
4+
$: mobile = width < 640;
5+
</script>
6+
7+
{width / mobile}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
import { run } from 'svelte/legacy';
3+
4+
let width = 0;
5+
let mobile = $derived(width < 640);
6+
run(() => {
7+
console.log(mobile);
8+
});
9+
</script>
10+
11+
{width / mobile}

0 commit comments

Comments
 (0)