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 { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import * as w from '../../warnings.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 * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.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 * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.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 */ /** 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); export const block_open = b.literal(BLOCK_OPEN);
@ -315,7 +315,7 @@ export class PromiseOptimiser {
const promises = b.array( const promises = b.array(
this.expressions.map((expression) => { this.expressions.map((expression) => {
return expression.type === 'AwaitExpression' && !has_await(expression.argument) return expression.type === 'AwaitExpression' && !has_await_expression(expression.argument)
? expression.argument ? expression.argument
: b.call(b.thunk(expression, true)); : 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; let has_await = false;
walk(expression, null, { walk(node, null, {
AwaitExpression(_node, context) { AwaitExpression(_node, context) {
has_await = true; has_await = true;
context.stop(); context.stop();
} },
// don't traverse into these
FunctionDeclaration() {},
FunctionExpression() {},
ArrowFunctionExpression() {}
}); });
return has_await; return has_await;

@ -2,7 +2,7 @@
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { regex_is_valid_identifier } from '../phases/patterns.js'; import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.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 * @param {Array<ESTree.Expression | ESTree.SpreadElement | null>} elements
@ -451,7 +451,7 @@ export function thunk(expression, async = false) {
export function unthunk(expression) { export function unthunk(expression) {
// optimize `async () => await x()`, but not `async () => await x(await y)` // optimize `async () => await x()`, but not `async () => await x(await y)`
if (expression.async && expression.body.type === 'AwaitExpression') { 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)); return unthunk(arrow(expression.params, expression.body.argument));
} }
} }

@ -14,7 +14,10 @@ export const DESTROYED = 1 << 14;
// Flags exclusive to effects // Flags exclusive to effects
export const EFFECT_RAN = 1 << 15; 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 EFFECT_TRANSPARENT = 1 << 16;
export const EAGER_EFFECT = 1 << 17; export const EAGER_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18; export const HEAD_EFFECT = 1 << 18;

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

@ -149,6 +149,9 @@ function create_effect(type, fn, sync, push = true) {
(e.f & EFFECT_PRESERVED) === 0 (e.f & EFFECT_PRESERVED) === 0
) { ) {
e = e.first; e = e.first;
if ((type & BLOCK_EFFECT) !== 0 && (type & EFFECT_TRANSPARENT) !== 0 && e !== null) {
e.f |= EFFECT_TRANSPARENT;
}
} }
if (e !== null) { if (e !== null) {
@ -604,7 +607,12 @@ export function pause_children(effect, transitions, local) {
while (child !== null) { while (child !== null) {
var sibling = child.next; 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 // 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 // it's slightly more involved though as we have to account for `transparent` changing
// through the tree. // 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