fix: don't eagerly access not-yet-initialized functions in template (#17938)

Our "hey this is a thunk invoking a function, let's flatten that" logic
caused a bug where lazily-initialized functions where eagerly referenced
in the template effect. That causes a nullpointer.

Ensuring these variables are always referenced in a closure fixes #17404

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/17936/head
Simon H 3 days ago committed by GitHub
parent b472171de6
commit 32a48ed174
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't eagerly access not-yet-initialized functions in template

@ -82,12 +82,16 @@ export class Memoizer {
async_values() {
if (this.#async.length === 0) return;
return b.array(this.#async.map((memo) => b.thunk(memo.expression, true)));
// use `b.arrow` rather than `b.thunk` so that deferred async/template effects
// always read live bindings rather than a possibly stale snapshot.
return b.array(this.#async.map((memo) => b.arrow([], memo.expression, true)));
}
sync_values() {
if (this.#sync.length === 0) return;
return b.array(this.#sync.map((memo) => b.thunk(memo.expression)));
// use `b.arrow` rather than `b.thunk` so that deferred async/template effects
// always read live bindings rather than a possibly stale snapshot.
return b.array(this.#sync.map((memo) => b.arrow([], memo.expression)));
}
}

@ -36,6 +36,13 @@ export function assignment_pattern(left, right) {
* @returns {ESTree.ArrowFunctionExpression}
*/
export function arrow(params, body, async = false) {
// optimize `async () => await x()`, but not `async () => await x(await y)`
if (async && body.type === 'AwaitExpression') {
if (!has_await_expression(body.argument)) {
return arrow(params, body.argument);
}
}
return {
type: 'ArrowFunctionExpression',
params,
@ -462,13 +469,6 @@ export function thunk(expression, async = false) {
* @returns {ESTree.Expression}
*/
export function unthunk(expression) {
// optimize `async () => await x()`, but not `async () => await x(await y)`
if (expression.async && expression.body.type === 'AwaitExpression') {
if (!has_await_expression(expression.body.argument)) {
return unthunk(arrow(expression.params, expression.body.argument));
}
}
if (
expression.async === false &&
expression.body.type === 'CallExpression' &&

@ -0,0 +1,9 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, 'aaa 1');
}
});

@ -0,0 +1,10 @@
<script>
let name = $derived(await new Promise((a) => a('aaa')));
function use() {
return () => 1;
}
const aa = use();
</script>
{name}
{aa()}

@ -32,7 +32,7 @@ export default function Main($$anchor) {
$.set_attribute(div_1, 'foobar', $0);
$.set_attribute(svg_1, 'viewBox', $1);
},
[y, y]
[() => y(), () => y()]
);
$.append($$anchor, fragment);

@ -19,6 +19,6 @@ export default function Text_nodes_deriveds($$anchor) {
var text = $.child(p);
$.reset(p);
$.template_effect(($0, $1) => $.set_text(text, `${$0 ?? ''}${$1 ?? ''}`), [text1, text2]);
$.template_effect(($0, $1) => $.set_text(text, `${$0 ?? ''}${$1 ?? ''}`), [() => text1(), () => text2()]);
$.append($$anchor, p);
}
Loading…
Cancel
Save