Merge branch 'main' into forking-hell

pull/17004/head
Rich Harris 1 week ago
commit c8cc5e6e00

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: take into account static blocks when determining transition locality

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: coordinate mount of snippets with await expressions

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: better optimization of await expressions

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: flush pending changes after rendering `failed` snippet

@ -6,7 +6,7 @@ import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { extract_identifiers } from '../../utils/ast.js';
import { extract_identifiers, has_await_expression } from '../../utils/ast.js';
import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';

@ -12,7 +12,7 @@ import {
import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.js';
import { has_await } from '../../../../../utils/ast.js';
import { has_await_expression } from '../../../../../utils/ast.js';
/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
export const block_open = b.literal(BLOCK_OPEN);
@ -315,7 +315,7 @@ export class PromiseOptimiser {
const promises = b.array(
this.expressions.map((expression) => {
return expression.type === 'AwaitExpression' && !has_await(expression.argument)
return expression.type === 'AwaitExpression' && !has_await_expression(expression.argument)
? expression.argument
: b.call(b.thunk(expression, true));
})

@ -611,16 +611,20 @@ export function build_assignment_value(operator, left, right) {
}
/**
* @param {ESTree.Expression} expression
* @param {ESTree.Node} node
*/
export function has_await(expression) {
export function has_await_expression(node) {
let has_await = false;
walk(expression, null, {
walk(node, null, {
AwaitExpression(_node, context) {
has_await = true;
context.stop();
}
},
// don't traverse into these
FunctionDeclaration() {},
FunctionExpression() {},
ArrowFunctionExpression() {}
});
return has_await;

@ -2,7 +2,7 @@
import { walk } from 'zimmerframe';
import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.js';
import { has_await } from './ast.js';
import { has_await_expression } from './ast.js';
/**
* @param {Array<ESTree.Expression | ESTree.SpreadElement | null>} elements
@ -451,7 +451,7 @@ export function thunk(expression, async = false) {
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.body.argument)) {
if (!has_await_expression(expression.body.argument)) {
return unthunk(arrow(expression.params, expression.body.argument));
}
}

@ -14,7 +14,10 @@ export const DESTROYED = 1 << 14;
// Flags exclusive to effects
export const EFFECT_RAN = 1 << 15;
/** 'Transparent' effects do not create a transition boundary */
/**
* 'Transparent' effects do not create a transition boundary.
* This is on a block effect 99% of the time but may also be on a branch effect if its parent block effect was pruned
*/
export const EFFECT_TRANSPARENT = 1 << 16;
export const EAGER_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;

@ -30,7 +30,6 @@ import {
skip_nodes,
set_hydrate_node
} from '../hydration.js';
import { get_next_sibling } from '../operations.js';
import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
@ -39,6 +38,7 @@ import { Batch, effect_pending_updates } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
import { create_text } from '../operations.js';
/**
* @typedef {{
@ -93,6 +93,9 @@ export class Boundary {
/** @type {DocumentFragment | null} */
#offscreen_fragment = null;
/** @type {TemplateNode | null} */
#pending_anchor = null;
#local_pending_count = 0;
#pending_count = 0;
@ -156,8 +159,10 @@ export class Boundary {
this.#hydrate_resolved_content();
}
} else {
var anchor = this.#get_anchor();
try {
this.#main_effect = branch(() => children(this.#anchor));
this.#main_effect = branch(() => children(anchor));
} catch (error) {
this.error(error);
}
@ -168,6 +173,10 @@ export class Boundary {
this.#pending = false;
}
}
return () => {
this.#pending_anchor?.remove();
};
}, flags);
if (hydrating) {
@ -195,9 +204,11 @@ export class Boundary {
this.#pending_effect = branch(() => pending(this.#anchor));
Batch.enqueue(() => {
var anchor = this.#get_anchor();
this.#main_effect = this.#run(() => {
Batch.ensure();
return branch(() => this.#children(this.#anchor));
return branch(() => this.#children(anchor));
});
if (this.#pending_count > 0) {
@ -212,6 +223,19 @@ export class Boundary {
});
}
#get_anchor() {
var anchor = this.#anchor;
if (this.#pending) {
this.#pending_anchor = create_text();
this.#anchor.before(this.#pending_anchor);
anchor = this.#pending_anchor;
}
return anchor;
}
/**
* Returns `true` if the effect exists inside a boundary whose pending snippet is shown
* @returns {boolean}
@ -253,6 +277,7 @@ export class Boundary {
if (this.#main_effect !== null) {
this.#offscreen_fragment = document.createDocumentFragment();
this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor));
move_effect(this.#main_effect, this.#offscreen_fragment);
}
@ -402,6 +427,7 @@ export class Boundary {
if (failed) {
queue_micro_task(() => {
this.#failed_effect = this.#run(() => {
Batch.ensure();
this.#is_creating_fallback = true;
try {

@ -149,6 +149,9 @@ function create_effect(type, fn, sync, push = true) {
(e.f & EFFECT_PRESERVED) === 0
) {
e = e.first;
if ((type & BLOCK_EFFECT) !== 0 && (type & EFFECT_TRANSPARENT) !== 0 && e !== null) {
e.f |= EFFECT_TRANSPARENT;
}
}
if (e !== null) {
@ -604,7 +607,12 @@ export function pause_children(effect, transitions, local) {
while (child !== null) {
var sibling = child.next;
var transparent = (child.f & EFFECT_TRANSPARENT) !== 0 || (child.f & BRANCH_EFFECT) !== 0;
var transparent =
(child.f & EFFECT_TRANSPARENT) !== 0 ||
// If this is a branch effect without a block effect parent,
// it means the parent block effect was pruned. In that case,
// transparency information was transferred to the branch effect.
((child.f & BRANCH_EFFECT) !== 0 && (effect.f & BLOCK_EFFECT) !== 0);
// TODO we don't need to call pause_children recursively with a linked list in place
// it's slightly more involved though as we have to account for `transparent` changing
// through the tree.

@ -0,0 +1,7 @@
<script>
let { children, push } = $props();
let message = await push('hello from child');
</script>
<p>message: {message}</p>
{@render children()}

@ -0,0 +1,25 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [shift] = target.querySelectorAll('button');
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>shift</button><p>loading...</p>`);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>shift</button>
<p>message: hello from child</p>
<p>hello from parent</p>
`
);
}
});

@ -0,0 +1,21 @@
<script>
import Child from './Child.svelte';
const resolvers = [];
function push(value) {
const { promise, resolve } = Promise.withResolvers();
resolvers.push(() => resolve(value));
return promise;
}
</script>
<button onclick={() => resolvers.shift()?.()}>shift</button>
<svelte:boundary>
<Child {push}>
<p>{await push('hello from parent')}</p>
</Child>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,12 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const btn = target.querySelector('button');
btn?.click();
await tick();
assert.deepEqual(logs, ['attachment']);
}
});

@ -0,0 +1,20 @@
<script>
let fail = $state(false);
function error() {
throw new Error('oops');
}
function attachment() {
console.log('attachment');
}
</script>
<svelte:boundary>
{fail ? error() : 'all good'}
<button onclick={() => fail = true}>fail</button>
{#snippet failed()}
<div {@attach attachment}>oops!</div>
{/snippet}
</svelte:boundary>

@ -0,0 +1,22 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`
<button>Toggle</button>
<div>Should not transition out</div>
`
);
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, '<button>Toggle</button>');
}
});

@ -0,0 +1,18 @@
<script>
import { slide } from 'svelte/transition';
let showText = $state(false);
let show = $state(true);
</script>
<button onclick={() => showText = !showText}>
Toggle
</button>
{#if showText}
{#if show}
<div transition:slide>
Should not transition out
</div>
{/if}
{/if}
Loading…
Cancel
Save