Merge branch 'main' into failing-test-derived-fork

failing-test-derived-fork
Paolo Ricciuti 20 hours ago committed by GitHub
commit e3ecd2a80b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +1,27 @@
# svelte
## 5.43.10
### Patch Changes
- fix: avoid other batches running with queued root effects of main batch ([#17145](https://github.com/sveltejs/svelte/pull/17145))
## 5.43.9
### Patch Changes
- fix: correctly handle functions when determining async blockers ([#17137](https://github.com/sveltejs/svelte/pull/17137))
- fix: keep deriveds reactive after their original parent effect was destroyed ([#17171](https://github.com/sveltejs/svelte/pull/17171))
- fix: ensure eager effects don't break reactions chain ([#17138](https://github.com/sveltejs/svelte/pull/17138))
- fix: ensure async `@const` in boundary hydrates correctly ([#17165](https://github.com/sveltejs/svelte/pull/17165))
- fix: take blockers into account when creating `#await` blocks ([#17137](https://github.com/sveltejs/svelte/pull/17137))
- fix: parallelize async `@const`s in the template ([#17165](https://github.com/sveltejs/svelte/pull/17165))
## 5.43.8
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.43.8",
"version": "5.43.10",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -10,8 +10,7 @@ export function create_fragment(transparent = false) {
nodes: [],
metadata: {
transparent,
dynamic: false,
has_await: false
dynamic: false
}
};
}

@ -687,193 +687,7 @@ export function analyze_component(root, source, options) {
}
}
/**
* @param {ESTree.Node} expression
* @param {Scope} scope
* @param {Set<Binding>} touched
* @param {Set<ESTree.Node>} seen
*/
const touch = (expression, scope, touched, seen = new Set()) => {
if (seen.has(expression)) return;
seen.add(expression);
walk(
expression,
{ scope },
{
ImportDeclaration(node) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
touched.add(binding);
for (const assignment of binding.assignments) {
touch(assignment.value, assignment.scope, touched, seen);
}
}
}
}
}
);
};
/**
* @param {ESTree.Node} node
* @param {Set<ESTree.Node>} seen
* @param {Set<Binding>} reads
* @param {Set<Binding>} writes
*/
const trace_references = (node, reads, writes, seen = new Set()) => {
if (seen.has(node)) return;
seen.add(node);
/**
* @param {ESTree.Pattern} node
* @param {Scope} scope
*/
function update(node, scope) {
for (const pattern of unwrap_pattern(node)) {
const node = object(pattern);
if (!node) return;
const binding = scope.get(node.name);
if (!binding) return;
writes.add(binding);
}
}
walk(
node,
{ scope: instance.scope },
{
_(node, context) {
const scope = scopes.get(node);
if (scope) {
context.next({ scope });
} else {
context.next();
}
},
AssignmentExpression(node, context) {
update(node.left, context.state.scope);
},
UpdateExpression(node, context) {
update(
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument),
context.state.scope
);
},
CallExpression(node, context) {
// for now, assume everything touched by the callee ends up mutating the object
// TODO optimise this better
// special case — no need to peek inside effects as they only run once async work has completed
const rune = get_rune(node, context.state.scope);
if (rune === '$effect') return;
/** @type {Set<Binding>} */
const touched = new Set();
touch(node, context.state.scope, touched);
for (const b of touched) {
writes.add(b);
}
},
// don't look inside functions until they are called
ArrowFunctionExpression(_, context) {},
FunctionDeclaration(_, context) {},
FunctionExpression(_, context) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
reads.add(binding);
}
}
}
}
);
};
let awaited = false;
// TODO this should probably be attached to the scope?
var promises = b.id('$$promises');
/**
* @param {ESTree.Identifier} id
* @param {ESTree.Expression} blocker
*/
function push_declaration(id, blocker) {
analysis.instance_body.declarations.push(id);
const binding = /** @type {Binding} */ (instance.scope.get(id.name));
binding.blocker = blocker;
}
for (let node of instance.ast.body) {
if (node.type === 'ImportDeclaration') {
analysis.instance_body.hoisted.push(node);
continue;
}
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
// these can't exist inside `<script>` but TypeScript doesn't know that
continue;
}
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
node = node.declaration;
} else {
continue;
}
}
const has_await = has_await_expression(node);
awaited ||= has_await;
if (awaited && node.type !== 'FunctionDeclaration') {
/** @type {Set<Binding>} */
const reads = new Set(); // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(node, reads, writes);
const blocker = b.member(promises, b.literal(analysis.instance_body.async.length), true);
for (const binding of writes) {
binding.blocker = blocker;
}
if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations) {
for (const id of extract_identifiers(declarator.id)) {
push_declaration(id, blocker);
}
// one declarator per declaration, makes things simpler
analysis.instance_body.async.push({
node: declarator,
has_await
});
}
} else if (node.type === 'ClassDeclaration') {
push_declaration(node.id, blocker);
analysis.instance_body.async.push({ node, has_await });
} else {
analysis.instance_body.async.push({ node, has_await });
}
} else {
analysis.instance_body.sync.push(node);
}
}
calculate_blockers(instance, scopes, analysis);
if (analysis.runes) {
const props_refs = module.scope.references.get('$$props');
@ -1118,6 +932,282 @@ export function analyze_component(root, source, options) {
return analysis;
}
/**
* Analyzes the instance's top level statements to calculate which bindings need to wait on which
* top level statements. This includes indirect blockers such as functions referencing async top level statements.
*
* @param {Js} instance
* @param {Map<AST.SvelteNode, Scope>} scopes
* @param {ComponentAnalysis} analysis
* @returns {void}
*/
function calculate_blockers(instance, scopes, analysis) {
/**
* @param {ESTree.Node} expression
* @param {Scope} scope
* @param {Set<Binding>} touched
* @param {Set<ESTree.Node>} seen
*/
const touch = (expression, scope, touched, seen = new Set()) => {
if (seen.has(expression)) return;
seen.add(expression);
walk(
expression,
{ scope },
{
ImportDeclaration(node) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
touched.add(binding);
for (const assignment of binding.assignments) {
touch(assignment.value, assignment.scope, touched, seen);
}
}
}
}
}
);
};
/**
* @param {ESTree.Node} node
* @param {Set<ESTree.Node>} seen
* @param {Set<Binding>} reads
* @param {Set<Binding>} writes
*/
const trace_references = (node, reads, writes, seen = new Set()) => {
if (seen.has(node)) return;
seen.add(node);
/**
* @param {ESTree.Pattern} node
* @param {Scope} scope
*/
function update(node, scope) {
for (const pattern of unwrap_pattern(node)) {
const node = object(pattern);
if (!node) return;
const binding = scope.get(node.name);
if (!binding) return;
writes.add(binding);
}
}
walk(
node,
{ scope: instance.scope },
{
_(node, context) {
const scope = scopes.get(node);
if (scope) {
context.next({ scope });
} else {
context.next();
}
},
AssignmentExpression(node, context) {
update(node.left, context.state.scope);
},
UpdateExpression(node, context) {
update(
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument),
context.state.scope
);
},
CallExpression(node, context) {
// for now, assume everything touched by the callee ends up mutating the object
// TODO optimise this better
// special case — no need to peek inside effects as they only run once async work has completed
const rune = get_rune(node, context.state.scope);
if (rune === '$effect') return;
/** @type {Set<Binding>} */
const touched = new Set();
touch(node, context.state.scope, touched);
for (const b of touched) {
writes.add(b);
}
},
// don't look inside functions until they are called
ArrowFunctionExpression(_, context) {},
FunctionDeclaration(_, context) {},
FunctionExpression(_, context) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
reads.add(binding);
}
}
}
}
);
};
let awaited = false;
// TODO this should probably be attached to the scope?
const promises = b.id('$$promises');
/**
* @param {ESTree.Identifier} id
* @param {NonNullable<Binding['blocker']>} blocker
*/
function push_declaration(id, blocker) {
analysis.instance_body.declarations.push(id);
const binding = /** @type {Binding} */ (instance.scope.get(id.name));
binding.blocker = blocker;
}
/**
* Analysis of blockers for functions is deferred until we know which statements are async/blockers
* @type {Array<ESTree.FunctionDeclaration | ESTree.VariableDeclarator>}
*/
const functions = [];
for (let node of instance.ast.body) {
if (node.type === 'ImportDeclaration') {
analysis.instance_body.hoisted.push(node);
continue;
}
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
// these can't exist inside `<script>` but TypeScript doesn't know that
continue;
}
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
node = node.declaration;
} else {
continue;
}
}
const has_await = has_await_expression(node);
awaited ||= has_await;
if (node.type === 'FunctionDeclaration') {
analysis.instance_body.sync.push(node);
functions.push(node);
} else if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations) {
if (
declarator.init?.type === 'ArrowFunctionExpression' ||
declarator.init?.type === 'FunctionExpression'
) {
// One declarator per declaration, makes things simpler. The ternary ensures more accurate source maps in the common case
analysis.instance_body.sync.push(
node.declarations.length === 1 ? node : b.declaration(node.kind, [declarator])
);
functions.push(declarator);
} else if (!awaited) {
// One declarator per declaration, makes things simpler. The ternary ensures more accurate source maps in the common case
analysis.instance_body.sync.push(
node.declarations.length === 1 ? node : b.declaration(node.kind, [declarator])
);
} else {
/** @type {Set<Binding>} */
const reads = new Set(); // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(declarator, reads, writes);
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b.member(promises, b.literal(analysis.instance_body.async.length), true)
);
for (const binding of writes) {
binding.blocker = blocker;
}
for (const id of extract_identifiers(declarator.id)) {
push_declaration(id, blocker);
}
// one declarator per declaration, makes things simpler
analysis.instance_body.async.push({
node: declarator,
has_await
});
}
}
} else if (awaited) {
/** @type {Set<Binding>} */
const reads = new Set(); // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(node, reads, writes);
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b.member(promises, b.literal(analysis.instance_body.async.length), true)
);
for (const binding of writes) {
binding.blocker = blocker;
}
if (node.type === 'ClassDeclaration') {
push_declaration(node.id, blocker);
analysis.instance_body.async.push({ node, has_await });
} else {
analysis.instance_body.async.push({ node, has_await });
}
} else {
analysis.instance_body.sync.push(node);
}
}
for (const fn of functions) {
/** @type {Set<Binding>} */
const reads_writes = new Set();
const body =
fn.type === 'VariableDeclarator'
? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init).body
: fn.body;
trace_references(body, reads_writes, reads_writes);
const max = [...reads_writes].reduce((max, binding) => {
if (binding.blocker) {
let property = /** @type {ESTree.SimpleLiteral & { value: number }} */ (
binding.blocker.property
);
return Math.max(property.value, max);
}
return max;
}, -1);
if (max === -1) continue;
const blocker = b.member(promises, b.literal(max), true);
const binding = /** @type {Binding} */ (
fn.type === 'FunctionDeclaration'
? instance.scope.get(fn.id.name)
: instance.scope.get(/** @type {ESTree.Identifier} */ (fn.id).name)
);
binding.blocker = /** @type {typeof binding['blocker']} */ (blocker);
}
}
/**
* @param {Map<import('estree').LabeledStatement, ReactiveStatement>} unsorted_reactive_declarations
*/

@ -26,10 +26,6 @@ export function AwaitExpression(node, context) {
if (context.state.expression) {
context.state.expression.has_await = true;
if (context.state.fragment && context.path.some((node) => node.type === 'ConstTag')) {
context.state.fragment.metadata.has_await = true;
}
suspend = true;
}

@ -51,6 +51,11 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly after_update: Statement[];
/** Transformed `{@const }` declarations */
readonly consts: Statement[];
/** Transformed async `{@const }` declarations (if any) and those coming after them */
async_consts?: {
id: Identifier;
thunks: Expression[];
};
/** Transformed `let:` directives */
readonly let_directives: Statement[];
/** Memoized expressions */

@ -56,22 +56,36 @@ export function AwaitBlock(node, context) {
catch_block = b.arrow(args, b.block([...declarations, ...block.body]));
}
context.state.init.push(
add_svelte_meta(
b.call(
'$.await',
context.state.node,
expression,
node.pending
? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.pending)))
: b.null,
then_block,
catch_block
),
node,
'await'
)
const stmt = add_svelte_meta(
b.call(
'$.await',
context.state.node,
expression,
node.pending
? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.pending)))
: b.null,
then_block,
catch_block
),
node,
'await'
);
if (node.metadata.expression.has_blockers()) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([]),
b.arrow([context.state.node], b.block([stmt]))
)
)
);
} else {
context.state.init.push(stmt);
}
}
/**

@ -24,15 +24,15 @@ export function ConstTag(node, context) {
expression = b.call('$.tag', expression, b.literal(declaration.id.name));
}
context.state.consts.push(b.const(declaration.id, expression));
context.state.transform[declaration.id.name] = { read: get_value };
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
context.state.consts.push(b.stmt(b.call('$.get', declaration.id)));
}
add_const_declaration(
context.state,
declaration.id,
expression,
node.metadata.expression.has_await,
context.state.scope.get_bindings(declaration)
);
} else {
const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(context.state.scope.generate('computed_const'));
@ -69,13 +69,13 @@ export function ConstTag(node, context) {
expression = b.call('$.tag', expression, b.literal('[@const]'));
}
context.state.consts.push(b.const(tmp, expression));
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
context.state.consts.push(b.stmt(b.call('$.get', tmp)));
}
add_const_declaration(
context.state,
tmp,
expression,
node.metadata.expression.has_await,
context.state.scope.get_bindings(declaration)
);
for (const node of identifiers) {
context.state.transform[node.name] = {
@ -84,3 +84,39 @@ export function ConstTag(node, context) {
}
}
}
/**
* @param {ComponentContext['state']} state
* @param {import('estree').Identifier} id
* @param {import('estree').Expression} expression
* @param {boolean} has_await
* @param {import('#compiler').Binding[]} bindings
*/
function add_const_declaration(state, id, expression, has_await, bindings) {
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
const after = dev ? [b.stmt(b.call('$.get', id))] : [];
if (has_await || state.async_consts) {
const run = (state.async_consts ??= {
id: b.id(state.scope.generate('promises')),
thunks: []
});
state.consts.push(b.let(id));
const assignment = b.assignment('=', id, expression);
const body = after.length === 0 ? assignment : b.block([b.stmt(assignment), ...after]);
run.thunks.push(b.thunk(body, has_await));
const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true);
for (const binding of bindings) {
binding.blocker = blocker;
}
} else {
state.consts.push(b.const(id, expression));
state.consts.push(...after);
}
}

@ -48,8 +48,6 @@ export function Fragment(node, context) {
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
const has_await = context.state.init !== null && (node.metadata.has_await || false);
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
/** @type {Statement[]} */
@ -72,7 +70,8 @@ export function Fragment(node, context) {
metadata: {
namespace,
bound_contenteditable: context.state.metadata.bound_contenteditable
}
},
async_consts: undefined
};
for (const node of hoisted) {
@ -153,8 +152,8 @@ export function Fragment(node, context) {
body.push(...state.let_directives, ...state.consts);
if (has_await) {
body.push(b.if(b.call('$.aborted'), b.return()));
if (state.async_consts && state.async_consts.thunks.length > 0) {
body.push(b.var(state.async_consts.id, b.call('$.run', b.array(state.async_consts.thunks))));
}
if (is_text_first) {
@ -177,13 +176,5 @@ export function Fragment(node, context) {
body.push(close);
}
if (has_await) {
return b.block([
b.stmt(
b.call('$.async_body', b.id('$$anchor'), b.arrow([b.id('$$anchor')], b.block(body), true))
)
]);
} else {
return b.block(body);
}
return b.block(body);
}

@ -14,8 +14,6 @@ export function SnippetBlock(node, context) {
// TODO hoist where possible
/** @type {(Identifier | AssignmentPattern)[]} */
const args = [b.id('$$anchor')];
const has_await = node.body.metadata.has_await || false;
/** @type {BlockStatement} */
let body;
@ -78,12 +76,8 @@ export function SnippetBlock(node, context) {
// in dev we use a FunctionExpression (not arrow function) so we can use `arguments`
let snippet = dev
? b.call(
'$.wrap_snippet',
b.id(context.state.analysis.name),
b.function(null, args, body, has_await)
)
: b.arrow(args, body, has_await);
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body))
: b.arrow(args, body);
const declaration = b.const(node.expression, snippet);

@ -48,7 +48,11 @@ export function SvelteBoundary(node, context) {
if (child.type === 'ConstTag') {
has_const = true;
if (!context.state.options.experimental.async) {
context.visit(child, { ...context.state, consts: const_tags });
context.visit(child, {
...context.state,
consts: const_tags,
scope: context.state.scopes.get(node.fragment) ?? context.state.scope
});
}
}
}
@ -101,7 +105,13 @@ export function SvelteBoundary(node, context) {
nodes.push(child);
}
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
const block = /** @type {BlockStatement} */ (
context.visit(
{ ...node.fragment, nodes },
// Since we're creating a new fragment the reference in scopes can't match, so we gotta attach the right scope manually
{ ...context.state, scope: context.state.scopes.get(node.fragment) ?? context.state.scope }
)
);
if (!context.state.options.experimental.async) {
block.body.unshift(...const_tags);

@ -105,7 +105,7 @@ export function process_children(nodes, initial, is_element, context) {
is_element &&
// In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled
// TODO switch this around and instead optimize for elements with a single block child and not require extra comments (neither for async nor normally)
!(node.body.metadata.has_await || node.metadata.expression.is_async())
!node.metadata.expression.is_async()
) {
node.metadata.is_controlled = true;
} else {

@ -1,4 +1,10 @@
import type { Expression, Statement, ModuleDeclaration, LabeledStatement } from 'estree';
import type {
Expression,
Statement,
ModuleDeclaration,
LabeledStatement,
Identifier
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
@ -21,6 +27,11 @@ export interface ComponentServerTransformState extends ServerTransformState {
readonly namespace: Namespace;
readonly preserve_whitespace: boolean;
readonly skip_hydration_boundaries: boolean;
/** Transformed async `{@const }` declarations (if any) and those coming after them */
async_consts?: {
id: Identifier;
thunks: Expression[];
};
}
export type Context = import('zimmerframe').Context<AST.SvelteNode, ServerTransformState>;

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { extract_identifiers } from '../../../../utils/ast.js';
/**
* @param {AST.ConstTag} node
@ -11,6 +12,29 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
const id = /** @type {Pattern} */ (context.visit(declaration.id));
const init = /** @type {Expression} */ (context.visit(declaration.init));
const has_await = node.metadata.expression.has_await;
context.state.init.push(b.const(id, init));
if (has_await || context.state.async_consts) {
const run = (context.state.async_consts ??= {
id: b.id(context.state.scope.generate('promises')),
thunks: []
});
const identifiers = extract_identifiers(declaration.id);
const bindings = context.state.scope.get_bindings(declaration);
for (const identifier of identifiers) {
context.state.init.push(b.let(identifier.name));
}
const assignment = b.assignment('=', id, init);
run.thunks.push(b.thunk(b.block([b.stmt(assignment)]), has_await));
const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true);
for (const binding of bindings) {
binding.blocker = blocker;
}
} else {
context.state.init.push(b.const(id, init));
}
}

@ -34,11 +34,7 @@ export function EachBlock(node, context) {
const new_body = /** @type {BlockStatement} */ (context.visit(node.body)).body;
if (node.body)
each.push(
// TODO get rid of fragment.has_await
...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_body)
);
if (node.body) each.push(...new_body);
const for_loop = b.for(
b.declaration('let', [
@ -61,7 +57,7 @@ export function EachBlock(node, context) {
b.if(
b.binary('!==', b.member(array_id, 'length'), b.literal(0)),
b.block([open, for_loop]),
node.fallback.metadata.has_await ? create_async_block(fallback) : fallback
fallback
)
);
} else {

@ -28,7 +28,8 @@ export function Fragment(node, context) {
init: [],
template: [],
namespace,
skip_hydration_boundaries: is_standalone
skip_hydration_boundaries: is_standalone,
async_consts: undefined
};
for (const node of hoisted) {
@ -42,5 +43,11 @@ export function Fragment(node, context) {
process_children(trimmed, { ...context, state });
if (state.async_consts && state.async_consts.thunks.length > 0) {
state.init.push(
b.var(state.async_consts.id, b.call('$$renderer.run', b.array(state.async_consts.thunks)))
);
}
return b.block([...state.init, ...build_template(state.template)]);
}

@ -25,11 +25,7 @@ export function IfBlock(node, context) {
const is_async = node.metadata.expression.is_async();
const has_await =
node.metadata.expression.has_await ||
// TODO get rid of this stuff
node.consequent.metadata.has_await ||
node.alternate?.metadata.has_await;
const has_await = node.metadata.expression.has_await;
if (is_async || has_await) {
statement = create_async_block(

@ -3,7 +3,6 @@
/** @import { ComponentContext } from '../types.js' */
import { dev } from '../../../../state.js';
import * as b from '#compiler/builders';
import { create_async_block } from './shared/utils.js';
/**
* @param {AST.SnippetBlock} node
@ -16,10 +15,6 @@ export function SnippetBlock(node, context) {
/** @type {BlockStatement} */ (context.visit(node.body))
);
if (node.body.metadata.has_await) {
fn.body = b.block([create_async_block(fn.body)]);
}
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;

@ -7,8 +7,7 @@ import {
block_open,
block_open_else,
build_attribute_value,
build_template,
create_async_block
build_template
} from './shared/utils.js';
/**
@ -43,14 +42,11 @@ export function SvelteBoundary(node, context) {
);
const pending = b.call(callee, b.id('$$renderer'));
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
const statement = node.fragment.metadata.has_await
? create_async_block(b.block([block]))
: block;
context.state.template.push(
b.if(
callee,
b.block(build_template([block_open_else, b.stmt(pending), block_close])),
b.block(build_template([block_open, statement, block_close]))
b.block(build_template([block_open, block, block_close]))
)
);
} else {
@ -70,9 +66,6 @@ export function SvelteBoundary(node, context) {
}
} else {
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
const statement = node.fragment.metadata.has_await
? create_async_block(b.block([block]))
: block;
context.state.template.push(block_open, statement, block_close);
context.state.template.push(block_open, block, block_close);
}
}

@ -244,12 +244,7 @@ export function build_inline_component(node, expression, context) {
params.push(pattern);
}
const slot_fn = b.arrow(
params,
node.fragment.metadata.has_await
? b.block([create_async_block(b.block(block.body))])
: b.block(block.body)
);
const slot_fn = b.arrow(params, b.block(block.body));
if (slot_name === 'default' && !has_children_prop) {
if (

@ -112,6 +112,10 @@ export class ExpressionMetadata {
return b.array([...this.#get_blockers()]);
}
has_blockers() {
return this.#get_blockers().size > 0;
}
is_async() {
return this.has_await || this.#get_blockers().size > 0;
}

@ -1,4 +1,4 @@
/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super } from 'estree' */
/** @import { BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super, SimpleLiteral, FunctionExpression, ArrowFunctionExpression } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference';
@ -108,7 +108,10 @@ export class Binding {
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
references = [];
/** @type {Array<{ value: Expression; scope: Scope }>} */
/**
* (Re)assignments of this binding. Includes declarations such as `function x() {}`.
* @type {Array<{ value: Expression; scope: Scope }>}
*/
assignments = [];
/**
@ -135,9 +138,10 @@ export class Binding {
/**
* Instance-level declarations may follow (or contain) a top-level `await`. In these cases,
* any reads that occur in the template must wait for the corresponding promise to resolve
* otherwise the initial value will not have been assigned
* otherwise the initial value will not have been assigned.
* It is a member expression of the form `$$blockers[n]`.
* TODO the blocker is set during transform which feels a bit grubby
* @type {Expression | null}
* @type {MemberExpression | null}
*/
blocker = null;

@ -48,8 +48,6 @@ export namespace AST {
* Whether or not we need to traverse into the fragment during mount/hydrate
*/
dynamic: boolean;
/** @deprecated we should get rid of this in favour of the `$$renderer.run` mechanism */
has_await: boolean;
};
}

@ -181,7 +181,7 @@ export function logical(operator, left, right) {
}
/**
* @param {'const' | 'let' | 'var'} kind
* @param {ESTree.VariableDeclaration['kind']} kind
* @param {ESTree.VariableDeclarator[]} declarations
* @returns {ESTree.VariableDeclaration}
*/

@ -244,7 +244,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
item.i = i;
}
batch.skipped_effects.delete(item.e);
if (defer) {
batch.skipped_effects.delete(item.e);
}
} else {
item = create_item(
first_run ? anchor : null,
@ -298,14 +300,14 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
set_hydrate_node(skip_nodes());
}
for (const [key, item] of state.items) {
if (!keys.has(key)) {
batch.skipped_effects.add(item.e);
}
}
if (!first_run) {
if (defer) {
for (const [key, item] of state.items) {
if (!keys.has(key)) {
batch.skipped_effects.add(item.e);
}
}
batch.oncommit(commit);
batch.ondiscard(() => {
// TODO presumably we need to do something here?

@ -68,6 +68,7 @@ export let previous_batch = null;
*/
export let batch_values = null;
// TODO this should really be a property of `batch`
/** @type {Effect[]} */
let queued_root_effects = [];
@ -171,6 +172,8 @@ export class Batch {
for (const root of root_effects) {
this.#traverse_effect_tree(root, target);
// Note: #traverse_effect_tree runs block effects eagerly, which can schedule effects,
// which means queued_root_effects now may be filled again.
}
if (!this.is_fork) {
@ -418,6 +421,10 @@ export class Batch {
// Re-run async/block effects that depend on distinct values changed in both batches
const others = [...batch.current.keys()].filter((s) => !this.current.has(s));
if (others.length > 0) {
// Avoid running queued root effects on the wrong branch
var prev_queued_root_effects = queued_root_effects;
queued_root_effects = [];
/** @type {Set<Value>} */
const marked = new Set();
/** @type {Map<Reaction, boolean>} */
@ -436,9 +443,10 @@ export class Batch {
// TODO do we need to do anything with `target`? defer block effects?
queued_root_effects = [];
batch.deactivate();
}
queued_root_effects = prev_queued_root_effects;
}
}

@ -11,7 +11,8 @@ import {
STALE_REACTION,
ASYNC,
WAS_MARKED,
CONNECTED
CONNECTED,
DESTROYED
} from '#client/constants';
import {
active_reaction,
@ -296,7 +297,9 @@ function get_derived_parent_effect(derived) {
var parent = derived.parent;
while (parent !== null) {
if ((parent.f & DERIVED) === 0) {
return /** @type {Effect} */ (parent);
// The original parent effect might've been destroyed but the derived
// is used elsewhere now - do not return the destroyed effect in that case
return (parent.f & DESTROYED) === 0 ? /** @type {Effect} */ (parent) : null;
}
parent = parent.parent;
}

@ -14,7 +14,9 @@ import {
is_dirty,
untracking,
is_destroying_effect,
push_reaction_value
push_reaction_value,
set_is_updating_effect,
is_updating_effect
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import {
@ -246,19 +248,25 @@ export function internal_set(source, value) {
export function flush_eager_effects() {
eager_effects_deferred = false;
var prev_is_updating_effect = is_updating_effect;
set_is_updating_effect(true);
const inspects = Array.from(eager_effects);
for (const effect of inspects) {
// Mark clean inspect-effects as maybe dirty and then check their dirtiness
// instead of just updating the effects - this way we avoid overfiring.
if ((effect.f & CLEAN) !== 0) {
set_signal_status(effect, MAYBE_DIRTY);
}
try {
for (const effect of inspects) {
// Mark clean inspect-effects as maybe dirty and then check their dirtiness
// instead of just updating the effects - this way we avoid overfiring.
if ((effect.f & CLEAN) !== 0) {
set_signal_status(effect, MAYBE_DIRTY);
}
if (is_dirty(effect)) {
update_effect(effect);
if (is_dirty(effect)) {
update_effect(effect);
}
}
} finally {
set_is_updating_effect(prev_is_updating_effect);
}
eager_effects.clear();

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.43.8';
export const VERSION = '5.43.10';
export const PUBLIC_VERSION = '5';

@ -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, '<p>1</p>');
}
});

@ -0,0 +1,7 @@
<script>
let foo = $derived(await 1);
</script>
{#await foo then x}
<p>{x}</p>
{/await}

@ -0,0 +1,15 @@
<script>
let open = $state(false);
let menuOptionsEl = $state(null);
</script>
<button onclick={() => open = !open}>
toggle
{#if open}
<!-- bind:this uses effect, which is scheduled, causing queued_root_effects to be filled again -->
<span bind:this={menuOptionsEl}>
A
</span>
{/if}
</button>

@ -0,0 +1,63 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [fork, commit, toggle] = target.querySelectorAll('button');
fork.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>fork</button>
<button>commit</button>
<button>toggle</button>
`
);
toggle.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>fork</button>
<button>commit</button>
<button>toggle <span>A</span></button>
`
);
toggle.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>fork</button>
<button>commit</button>
<button>toggle</button>
`
);
toggle.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>fork</button>
<button>commit</button>
<button>toggle <span>A</span></button>
`
);
commit.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>fork</button>
<button>commit</button>
B
`
);
}
});

@ -0,0 +1,24 @@
<script lang="ts">
import { fork } from 'svelte';
import A from './A.svelte';
import B from './B.svelte';
let open = $state(true);
let f;
</script>
<button onclick={() => {
f = fork(() => {
open = !open;
})
}}>fork</button>
<button onclick={() => {
f.commit()
}}>commit</button>
{#if open}
<A />
{:else}
<B />
{/if}

@ -2,11 +2,12 @@ import { tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `<h1>Loading...</h1>`,
mode: ['async-server', 'client', 'hydrate'],
ssrHtml: `<h1>Hello, world!</h1> 5 01234 5 sync 6 5 0`,
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, `<h1>Hello, world!</h1> 5 01234`);
assert.htmlEqual(target.innerHTML, `<h1>Hello, world!</h1> 5 01234 5 sync 6 5 0`);
}
});

@ -3,17 +3,16 @@
</script>
<svelte:boundary>
{@const sync = 'sync'}
{@const number = await Promise.resolve(5)}
{#snippet pending()}
<h1>Loading...</h1>
{/snippet}
{@const after_async = number + 1}
{@const { length, 0: first } = await '01234'}
{#snippet greet()}
{@const greeting = await `Hello, ${name}!`}
<h1>{greeting}</h1>
{number}
{#if number > 4}
{#if number > 4 && after_async && greeting}
{@const length = await number}
{#each { length }, index}
{@const i = await index}
@ -23,4 +22,5 @@
{/snippet}
{@render greet()}
{number} {sync} {after_async} {length} {first}
</svelte:boundary>

@ -0,0 +1,13 @@
<script>
await 1;
// Test arrow function style
let value = $state('');
const getValue = () => {
return value;
};
const setValue = (v) => { value = v }
</script>
<input bind:value={getValue, setValue} />
<p>{getValue()}</p>

@ -0,0 +1,11 @@
<script>
await 1;
// Test function declaration style
let value = $state('');
function getValue() { return value }
function setValue(v) { value = v }
</script>
<input bind:value={getValue, setValue} />
<p>{getValue()}</p>

@ -0,0 +1,16 @@
<script>
await 1;
// Test indirect blocker dependencies
let value = $state('');
function x() {
return value;
}
function getValue() {
return x()
}
function setValue(v) { value = v }
</script>
<input bind:value={getValue, setValue} />
<p>{getValue()}</p>

@ -0,0 +1,19 @@
<script>
await 1;
let value = $state('');
// getValue is declared BEFORE x
function getValue() {
return x()
}
function x() {
return value;
}
function setValue(v) { value = v }
</script>
<input bind:value={getValue, setValue} />
<p>{getValue()}</p>

@ -0,0 +1,27 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['async-server', 'client', 'hydrate'],
ssrHtml:
'<input value=""> <p></p> <input value=""> <p></p> <input value=""> <p></p> <input value=""> <p></p>',
async test({ assert, target }) {
await tick();
const inputs = Array.from(target.querySelectorAll('input'));
const paragraphs = Array.from(target.querySelectorAll('p'));
for (let i = 0; i < 4; i++) {
assert.equal(inputs[i].value, '');
assert.htmlEqual(paragraphs[i].innerHTML, '');
inputs[i].value = 'hello';
inputs[i].dispatchEvent(new InputEvent('input', { bubbles: true }));
await tick();
assert.equal(inputs[i].value, 'hello');
assert.htmlEqual(paragraphs[i].innerHTML, 'hello');
}
}
});

@ -0,0 +1,11 @@
<script>
import Component1 from './Component1.svelte';
import Component2 from './Component2.svelte';
import Component3 from './Component3.svelte';
import Component4 from './Component4.svelte';
</script>
<Component1 />
<Component2 />
<Component3 />
<Component4 />

@ -0,0 +1,35 @@
import { tick } from 'svelte';
import { test } from '../../test';
import { normalise_inspect_logs } from '../../../helpers';
export default test({
compileOptions: {
dev: true
},
async test({ assert, target, logs }) {
const [b] = target.querySelectorAll('button');
b.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>first unseen: 1</button>`);
b.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>first unseen: 2</button>`);
b.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>first unseen:</button>`);
assert.deepEqual(normalise_inspect_logs(logs), [
[0, 1, 2],
[1, 2],
'at SvelteSet.add',
[2],
'at SvelteSet.add',
[],
'at SvelteSet.add'
]);
}
});

@ -0,0 +1,14 @@
<script>
import {SvelteSet} from "svelte/reactivity";
const ids = [0,1,2];
const seenIds = new SvelteSet();
const unseenIds = $derived(ids.filter((id) => !seenIds.has(id)));
const currentId = $derived(unseenIds.at(0));
$inspect(unseenIds)
</script>
<button onclick={() => seenIds.add(currentId)}>
first unseen: {currentId}
</button>

@ -1391,6 +1391,33 @@ describe('signals', () => {
};
});
test('derived whose original parent effect has been destroyed keeps updating', () => {
return () => {
let count: Source<number>;
let double: Derived<number>;
const destroy = effect_root(() => {
render_effect(() => {
count = state(0);
double = derived(() => $.get(count) * 2);
});
});
flushSync();
assert.equal($.get(double!), 0);
destroy();
flushSync();
set(count!, 1);
flushSync();
assert.equal($.get(double!), 2);
set(count!, 2);
flushSync();
assert.equal($.get(double!), 4);
};
});
test('$effect.root inside deriveds stay alive independently', () => {
const log: any[] = [];
const c = state(0);

@ -25,20 +25,23 @@ export default function Async_in_derived($$anchor, $$props) {
{
var consequent = ($$anchor) => {
$.async_body($$anchor, async ($$anchor) => {
const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))();
const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))();
let yes1;
let yes2;
let no1;
let no2;
const no1 = $.derived(() => (async () => {
return await 1;
})());
var promises = $.run([
async () => yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(),
async () => yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(),
const no2 = $.derived(() => (async () => {
() => no1 = $.derived(() => (async () => {
return await 1;
})());
})()),
if ($.aborted()) return;
});
() => no2 = $.derived(() => (async () => {
return await 1;
})())
]);
};
$.if(node, ($$render) => {

@ -18,24 +18,38 @@ export default function Async_in_derived($$renderer, $$props) {
}
]);
$$renderer.async_block([], async ($$renderer) => {
if (true) {
$$renderer.push('<!--[-->');
const yes1 = (await $.save(1))();
const yes2 = foo((await $.save(1))());
const no1 = (async () => {
return await 1;
})();
const no2 = (async () => {
return await 1;
})();
} else {
$$renderer.push('<!--[!-->');
}
});
if (true) {
$$renderer.push('<!--[-->');
let yes1;
let yes2;
let no1;
let no2;
var promises = $$renderer.run([
async () => {
yes1 = (await $.save(1))();
},
async () => {
yes2 = foo((await $.save(1))());
},
() => {
no1 = (async () => {
return await 1;
})();
},
() => {
no2 = (async () => {
return await 1;
})();
}
]);
} else {
$$renderer.push('<!--[!-->');
}
$$renderer.push(`<!--]-->`);
});

Loading…
Cancel
Save