Merge branch 'main' into docs/bindable

pull/17121/head
Davi Oliveira da Silva 1 month ago committed by GitHub
commit 8f0f90389e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +0,0 @@
---
"svelte": patch
---
feat: experimental `fork` API

@ -149,7 +149,7 @@ This restriction only applies when using the `experimental.async` option, which
### fork_discarded
```
Cannot commit a fork that was already committed or discarded
Cannot commit a fork that was already discarded
```
### fork_timing

@ -1,5 +1,79 @@
# svelte
## 5.43.4
### Patch Changes
- chore: simplify connection/disconnection logic ([#17105](https://github.com/sveltejs/svelte/pull/17105))
- fix: reconnect deriveds to effect tree when time-travelling ([#17105](https://github.com/sveltejs/svelte/pull/17105))
## 5.43.3
### Patch Changes
- fix: ensure fork always accesses correct values ([#17098](https://github.com/sveltejs/svelte/pull/17098))
- fix: change title only after any pending work has completed ([#17061](https://github.com/sveltejs/svelte/pull/17061))
- fix: preserve symbols when creating derived rest properties ([#17096](https://github.com/sveltejs/svelte/pull/17096))
## 5.43.2
### Patch Changes
- fix: treat each blocks with async dependencies as uncontrolled ([#17077](https://github.com/sveltejs/svelte/pull/17077))
## 5.43.1
### Patch Changes
- fix: transform `$bindable` after `await` expressions ([#17066](https://github.com/sveltejs/svelte/pull/17066))
## 5.43.0
### Minor Changes
- feat: out-of-order rendering ([#17038](https://github.com/sveltejs/svelte/pull/17038))
### Patch Changes
- fix: settle batch after DOM updates ([#17054](https://github.com/sveltejs/svelte/pull/17054))
## 5.42.3
### Patch Changes
- fix: handle `<svelte:head>` rendered asynchronously ([#17052](https://github.com/sveltejs/svelte/pull/17052))
- fix: don't restore batch in `#await` ([#17051](https://github.com/sveltejs/svelte/pull/17051))
## 5.42.2
### Patch Changes
- fix: better error message for global variable assignments ([#17036](https://github.com/sveltejs/svelte/pull/17036))
- chore: tweak memoizer logic ([#17042](https://github.com/sveltejs/svelte/pull/17042))
## 5.42.1
### Patch Changes
- fix: ignore fork `discard()` after `commit()` ([#17034](https://github.com/sveltejs/svelte/pull/17034))
## 5.42.0
### Minor Changes
- feat: experimental `fork` API ([#17004](https://github.com/sveltejs/svelte/pull/17004))
### Patch Changes
- fix: always allow `setContext` before first await in component ([#17031](https://github.com/sveltejs/svelte/pull/17031))
- fix: less confusing names for inspect errors ([#17026](https://github.com/sveltejs/svelte/pull/17026))
## 5.41.4
### Patch Changes

@ -114,7 +114,7 @@ This restriction only applies when using the `experimental.async` option, which
## fork_discarded
> Cannot commit a fork that was already committed or discarded
> Cannot commit a fork that was already discarded
## fork_timing

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

@ -9,11 +9,10 @@ import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_fragment } from '../utils/create.js';
import { create_attribute, create_expression_metadata, is_element_node } from '../../nodes.js';
import { create_attribute, ExpressionMetadata, is_element_node } from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
import { regex_whitespace } from '../../patterns.js';
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
@ -297,7 +296,7 @@ export default function element(parser) {
element.tag = get_attribute_expression(definition);
}
element.metadata.expression = create_expression_metadata();
element.metadata.expression = new ExpressionMetadata();
}
if (is_top_level_script_or_style) {
@ -508,7 +507,7 @@ function read_attribute(parser) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
@ -528,7 +527,7 @@ function read_attribute(parser) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
@ -568,7 +567,7 @@ function read_attribute(parser) {
name
},
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
@ -628,7 +627,7 @@ function read_attribute(parser) {
modifiers: /** @type {Array<'important'>} */ (modifiers),
value,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
}
@ -658,7 +657,7 @@ function read_attribute(parser) {
name: directive_name,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
@ -824,7 +823,7 @@ function read_sequence(parser, done, location) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};

@ -3,7 +3,7 @@
/** @import { Parser } from '../index.js' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { create_expression_metadata } from '../../nodes.js';
import { ExpressionMetadata } from '../../nodes.js';
import { parse_expression_at } from '../acorn.js';
import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js';
@ -42,7 +42,7 @@ export default function tag(parser) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
}
@ -65,7 +65,7 @@ function open(parser) {
consequent: create_fragment(),
alternate: null,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -249,7 +249,7 @@ function open(parser) {
then: null,
catch: null,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -334,7 +334,7 @@ function open(parser) {
expression,
fragment: create_fragment(),
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -477,7 +477,7 @@ function next(parser) {
consequent: create_fragment(),
alternate: null,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -643,7 +643,7 @@ function special(parser) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -721,7 +721,7 @@ function special(parser) {
end: parser.index - 1
},
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
}
@ -748,7 +748,7 @@ function special(parser) {
end: parser.index,
expression: /** @type {AST.RenderTag['expression']} */ (expression),
metadata: {
expression: create_expression_metadata(),
expression: new ExpressionMetadata(),
dynamic: false,
arguments: [],
path: [],

@ -1,4 +1,4 @@
/** @import { Expression, Node, Program } from 'estree' */
/** @import * as ESTree from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
@ -6,7 +6,12 @@ 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, has_await_expression } from '../../utils/ast.js';
import {
extract_identifiers,
has_await_expression,
object,
unwrap_pattern
} 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';
@ -206,7 +211,7 @@ const visitors = {
* @returns {Js}
*/
function js(script, root, allow_reactive_declarations, parent) {
/** @type {Program} */
/** @type {ESTree.Program} */
const ast = script?.content ?? {
type: 'Program',
sourceType: 'module',
@ -289,7 +294,7 @@ export function analyze_module(source, options) {
});
walk(
/** @type {Node} */ (ast),
/** @type {ESTree.Node} */ (ast),
{
scope,
scopes,
@ -347,7 +352,7 @@ export function analyze_component(root, source, options) {
const store_name = name.slice(1);
const declaration = instance.scope.get(store_name);
const init = /** @type {Node | undefined} */ (declaration?.initial);
const init = /** @type {ESTree.Node | undefined} */ (declaration?.initial);
// If we're not in legacy mode through the compiler option, assume the user
// is referencing a rune and not a global store.
@ -407,7 +412,7 @@ export function analyze_component(root, source, options) {
/** @type {number} */ (node.start) > /** @type {number} */ (module.ast.start) &&
/** @type {number} */ (node.end) < /** @type {number} */ (module.ast.end) &&
// const state = $state(0) is valid
get_rune(/** @type {Node} */ (path.at(-1)), module.scope) === null
get_rune(/** @type {ESTree.Node} */ (path.at(-1)), module.scope) === null
) {
e.store_invalid_subscription(node);
}
@ -543,7 +548,13 @@ export function analyze_component(root, source, options) {
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set(),
pickled_awaits: new Set()
pickled_awaits: new Set(),
instance_body: {
sync: [],
async: [],
declarations: [],
hoisted: []
}
};
if (!runes) {
@ -636,7 +647,7 @@ export function analyze_component(root, source, options) {
// @ts-expect-error
_: set_scope,
Identifier(node, context) {
const parent = /** @type {Expression} */ (context.path.at(-1));
const parent = /** @type {ESTree.Expression} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
@ -676,6 +687,194 @@ 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);
}
}
if (analysis.runes) {
const props_refs = module.scope.references.get('$$props');
if (props_refs) {

@ -1,6 +1,7 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, ExpressionMetadata, StateField, ValidatedCompileOptions } from '#compiler';
import type { AST, StateField, ValidatedCompileOptions } from '#compiler';
import type { ExpressionMetadata } from '../nodes.js';
export interface AnalysisState {
scope: Scope;

@ -1,12 +1,7 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { AST, DelegatedEvent } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js';
import {
get_attribute_chunks,
get_attribute_expression,
is_event_attribute
} from '../../../utils/ast.js';
import { cannot_be_set_statically, can_delegate_event } from '../../../../utils.js';
import { get_attribute_chunks, is_event_attribute } from '../../../utils/ast.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
@ -64,181 +59,8 @@ export function Attribute(node, context) {
context.state.analysis.uses_event_attributes = true;
}
const expression = get_attribute_expression(node);
const delegated_event = get_delegated_event(node.name.slice(2), expression, context);
if (delegated_event !== null) {
if (delegated_event.hoisted) {
delegated_event.function.metadata.hoisted = true;
}
node.metadata.delegated = delegated_event;
}
}
}
}
/** @type {DelegatedEvent} */
const unhoisted = { hoisted: false };
/**
* Checks if given event attribute can be delegated/hoisted and returns the corresponding info if so
* @param {string} event_name
* @param {Expression | null} handler
* @param {Context} context
* @returns {null | DelegatedEvent}
*/
function get_delegated_event(event_name, handler, context) {
// Handle delegated event handlers. Bail out if not a delegated event.
if (!handler || !is_delegated(event_name)) {
return null;
}
// If we are not working with a RegularElement, then bail out.
const element = context.path.at(-1);
if (element?.type !== 'RegularElement') {
return null;
}
/** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */
let target_function = null;
let binding = null;
if (element.metadata.has_spread) {
// event attribute becomes part of the dynamic spread array
return unhoisted;
}
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
target_function = handler;
} else if (handler.type === 'Identifier') {
binding = context.state.scope.get(handler.name);
if (context.state.analysis.module.scope.references.has(handler.name)) {
// If a binding with the same name is referenced in the module scope (even if not declared there), bail out
return unhoisted;
}
if (binding != null) {
for (const { path } of binding.references) {
const parent = path.at(-1);
if (parent === undefined) return unhoisted;
const grandparent = path.at(-2);
/** @type {AST.RegularElement | null} */
let element = null;
/** @type {string | null} */
let event_name = null;
if (parent.type === 'OnDirective') {
element = /** @type {AST.RegularElement} */ (grandparent);
event_name = parent.name;
} else if (
parent.type === 'ExpressionTag' &&
grandparent?.type === 'Attribute' &&
is_event_attribute(grandparent)
) {
element = /** @type {AST.RegularElement} */ (path.at(-3));
const attribute = /** @type {AST.Attribute} */ (grandparent);
event_name = get_attribute_event_name(attribute.name);
}
if (element && event_name) {
if (
element.type !== 'RegularElement' ||
element.metadata.has_spread ||
!is_delegated(event_name)
) {
return unhoisted;
}
} else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') {
return unhoisted;
}
}
node.metadata.delegated =
parent?.type === 'RegularElement' && can_delegate_event(node.name.slice(2));
}
// If the binding is exported, bail out
if (context.state.analysis.exports.find((node) => node.name === handler.name)) {
return unhoisted;
}
if (binding?.is_function()) {
target_function = binding.initial;
}
}
// If we can't find a function, or the function has multiple parameters, bail out
if (target_function == null || target_function.params.length > 1) {
return unhoisted;
}
const visited_references = new Set();
const scope = target_function.metadata.scope;
for (const [reference] of scope.references) {
// Bail out if the arguments keyword is used or $host is referenced
if (reference === 'arguments' || reference === '$host') return unhoisted;
// Bail out if references a store subscription
if (scope.get(`$${reference}`)?.kind === 'store_sub') return unhoisted;
const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
// if the function access a snippet that can't be hoisted we bail out
if (
local_binding !== null &&
local_binding.initial?.type === 'SnippetBlock' &&
!local_binding.initial.metadata.can_hoist
) {
return unhoisted;
}
// If we are referencing a binding that is shadowed in another scope then bail out (unless it's declared within the function).
if (
local_binding !== null &&
binding !== null &&
local_binding.node !== binding.node &&
scope.declarations.get(reference) !== binding
) {
return unhoisted;
}
// If we have multiple references to the same store using $ prefix, bail out.
if (
binding !== null &&
binding.kind === 'store_sub' &&
visited_references.has(reference.slice(1))
) {
return unhoisted;
}
// If we reference the index within an each block, then bail out.
if (binding !== null && binding.initial?.type === 'EachBlock') return unhoisted;
if (
binding !== null &&
// Bail out if the binding is a rest param
(binding.declaration_kind === 'rest_param' ||
// Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
(((!context.state.analysis.runes && binding.kind === 'each') ||
// or any normal not reactive bindings that are mutated.
binding.kind === 'normal') &&
binding.updated))
) {
return unhoisted;
}
visited_references.add(reference);
}
return { hoisted: true, function: target_function };
}
/**
* @param {string} event_name
*/
function get_attribute_event_name(event_name) {
event_name = event_name.slice(2);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
}
return event_name;
}

@ -10,16 +10,13 @@ import * as e from '../../../errors.js';
export function AwaitExpression(node, context) {
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
// preserve context for
// a) top-level await and
// b) awaits that precede other expressions in template or `$derived(...)`
// preserve context for awaits that precede other expressions in template or `$derived(...)`
if (
tla ||
(is_reactive_expression(
is_reactive_expression(
context.path,
context.state.derived_function_depth === context.state.function_depth
) &&
!is_last_evaluated_expression(context.path, node))
!is_last_evaluated_expression(context.path, node)
) {
context.state.analysis.pickled_awaits.add(node);
}
@ -145,6 +142,9 @@ function is_last_evaluated_expression(path, node) {
if (node !== parent.expressions.at(-1)) return false;
break;
case 'VariableDeclarator':
return true;
default:
return false;
}

@ -172,6 +172,7 @@ export function BindDirective(node, context) {
}
const binding = context.state.scope.get(left.name);
node.metadata.binding = binding;
if (assignee.type === 'Identifier') {
// reassignment

@ -7,7 +7,7 @@ import { get_parent } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.js';
import * as b from '#compiler/builders';
import { create_expression_metadata } from '../../nodes.js';
import { ExpressionMetadata } from '../../nodes.js';
/**
* @param {CallExpression} node
@ -243,7 +243,7 @@ export function CallExpression(node, context) {
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$derived') {
const expression = create_expression_metadata();
const expression = new ExpressionMetadata();
context.next({
...context.state,

@ -5,7 +5,7 @@ import * as e from '../../../errors.js';
import { validate_opening_tag } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { is_resolved_snippet } from './shared/snippets.js';
import { create_expression_metadata } from '../../nodes.js';
import { ExpressionMetadata } from '../../nodes.js';
/**
* @param {AST.RenderTag} node
@ -57,7 +57,7 @@ export function RenderTag(node, context) {
context.visit(callee, { ...context.state, expression: node.metadata.expression });
for (const arg of expression.arguments) {
const metadata = create_expression_metadata();
const metadata = new ExpressionMetadata();
node.metadata.arguments.push(metadata);
context.visit(arg, {

@ -81,8 +81,13 @@ export function SnippetBlock(node, context) {
function can_hoist_snippet(scope, scopes, visited = new Set()) {
for (const [reference] of scope.references) {
const binding = scope.get(reference);
if (!binding) continue;
if (!binding || binding.scope.function_depth === 0) {
if (binding.blocker) {
return false;
}
if (binding.scope.function_depth === 0) {
continue;
}

@ -6,13 +6,6 @@
* @param {Context} context
*/
export function visit_function(node, context) {
// TODO retire this in favour of a more general solution based on bindings
node.metadata = {
hoisted: false,
hoisted_params: [],
scope: context.state.scope
};
if (context.state.expression) {
for (const [name] of context.state.scope.references) {
const binding = context.state.scope.get(name);

@ -22,7 +22,10 @@ export function validate_assignment(node, argument, context) {
const binding = context.state.scope.get(argument.name);
if (context.state.analysis.runes) {
if (binding?.node === context.state.analysis.props_id) {
if (
context.state.analysis.props_id != null &&
binding?.node === context.state.analysis.props_id
) {
e.constant_assignment(node, '$props.id()');
}

@ -33,7 +33,6 @@ import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js';
import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { LetDirective } from './visitors/LetDirective.js';
@ -111,7 +110,6 @@ const visitors = {
HtmlTag,
Identifier,
IfBlock,
ImportDeclaration,
KeyBlock,
LabeledStatement,
LetDirective,
@ -153,7 +151,7 @@ export function client_component(analysis, options) {
scope: analysis.module.scope,
scopes: analysis.module.scopes,
is_instance: false,
hoisted: [b.import_all('$', 'svelte/internal/client')],
hoisted: [b.import_all('$', 'svelte/internal/client'), ...analysis.instance_body.hoisted],
node: /** @type {any} */ (null), // populated by the root node
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),
@ -370,41 +368,22 @@ export function client_component(analysis, options) {
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
if (analysis.instance.has_await) {
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports'));
}
const body = b.block([
...store_setup,
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body),
...(should_inject_context && component_returned_object.length > 0
? [b.stmt(b.assignment('=', b.id('$$exports'), b.object(component_returned_object)))]
: []),
b.if(b.call('$.aborted'), b.return()),
.../** @type {ESTree.Statement[]} */ (template.body)
]);
component_block.body.push(
b.stmt(b.call(`$.async_body`, b.id('$$anchor'), b.arrow([b.id('$$anchor')], body, true)))
);
} else {
component_block.body.push(
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body)
);
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports', b.object(component_returned_object)));
}
component_block.body.unshift(...store_setup);
component_block.body.push(
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body)
);
if (!analysis.runes && analysis.needs_context) {
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)));
}
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports', b.object(component_returned_object)));
}
component_block.body.unshift(...store_setup);
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
if (!analysis.runes && analysis.needs_context) {
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)));
}
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
if (analysis.needs_mutation_validation) {
component_block.body.unshift(
b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))

@ -21,9 +21,6 @@ export interface ClientTransformState extends TransformState {
*/
readonly in_constructor: boolean;
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;
readonly transform: Record<
string,
{

@ -1,6 +1,6 @@
/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */
/** @import { BlockStatement, Expression, Identifier } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { ClientTransformState, ComponentClientTransformState } from './types.js' */
/** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '#compiler/builders';
@ -12,9 +12,6 @@ import {
PROPS_IS_UPDATED,
PROPS_IS_BINDABLE
} from '../../../../constants.js';
import { dev } from '../../../state.js';
import { walk } from 'zimmerframe';
import { validate_mutation } from './visitors/shared/utils.js';
/**
* @param {Binding} binding
@ -46,125 +43,6 @@ export function build_getter(node, state) {
return node;
}
/**
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
* @param {ComponentContext} context
* @returns {Pattern[]}
*/
function get_hoisted_params(node, context) {
const scope = context.state.scope;
/** @type {Identifier[]} */
const params = [];
/**
* We only want to push if it's not already present to avoid name clashing
* @param {Identifier} id
*/
function push_unique(id) {
if (!params.find((param) => param.name === id.name)) {
params.push(id);
}
}
for (const [reference] of scope.references) {
let binding = scope.get(reference);
if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) {
if (binding.kind === 'store_sub') {
// We need both the subscription for getting the value and the store for updating
push_unique(b.id(binding.node.name));
binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1)));
}
let expression = context.state.transform[reference]?.read(b.id(binding.node.name));
if (
// If it's a destructured derived binding, then we can extract the derived signal reference and use that.
// TODO this code is bad, we need to kill it
expression != null &&
typeof expression !== 'function' &&
expression.type === 'MemberExpression' &&
expression.object.type === 'CallExpression' &&
expression.object.callee.type === 'Identifier' &&
expression.object.callee.name === '$.get' &&
expression.object.arguments[0].type === 'Identifier'
) {
push_unique(b.id(expression.object.arguments[0].name));
} else if (
// If we are referencing a simple $$props value, then we need to reference the object property instead
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
!is_prop_source(binding, context.state)
) {
push_unique(b.id('$$props'));
} else if (
// imports don't need to be hoisted
binding.declaration_kind !== 'import'
) {
// create a copy to remove start/end tags which would mess up source maps
push_unique(b.id(binding.node.name));
// rest props are often accessed through the $$props object for optimization reasons,
// but we can't know if the delegated event handler will use it, so we need to add both as params
if (binding.kind === 'rest_prop' && context.state.analysis.runes) {
push_unique(b.id('$$props'));
}
}
}
}
if (dev) {
// this is a little hacky, but necessary for ownership validation
// to work inside hoisted event handlers
/**
* @param {AssignmentExpression | UpdateExpression} node
* @param {{ next: () => void, stop: () => void }} context
*/
function visit(node, { next, stop }) {
if (validate_mutation(node, /** @type {any} */ (context), node) !== node) {
params.push(b.id('$$ownership_validator'));
stop();
} else {
next();
}
}
walk(/** @type {Node} */ (node), null, {
AssignmentExpression: visit,
UpdateExpression: visit
});
}
return params;
}
/**
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
* @param {ComponentContext} context
* @returns {Pattern[]}
*/
export function build_hoisted_params(node, context) {
const hoisted_params = get_hoisted_params(node, context);
node.metadata.hoisted_params = hoisted_params;
/** @type {Pattern[]} */
const params = [];
if (node.params.length === 0) {
if (hoisted_params.length > 0) {
// For the event object
params.push(b.id(context.state.scope.generate('_')));
}
} else {
for (const param of node.params) {
params.push(/** @type {Pattern} */ (context.visit(param)));
}
}
params.push(...hoisted_params);
return params;
}
/**
* @param {Binding} binding
* @param {ComponentClientTransformState} state

@ -243,18 +243,29 @@ export function BindDirective(node, context) {
}
}
const defer =
node.name !== 'this' &&
parent.type === 'RegularElement' &&
parent.attributes.find((a) => a.type === 'UseDirective');
let statement = defer ? b.stmt(b.call('$.effect', b.thunk(call))) : b.stmt(call);
// TODO this doesn't account for function bindings
if (node.metadata.binding?.blocker) {
statement = b.stmt(
b.call(b.member(node.metadata.binding.blocker, b.id('then')), b.thunk(b.block([statement])))
);
}
// Bindings need to happen after attribute updates, therefore after the render effect, and in order with events/actions.
// bind:this is a special case as it's one-way and could influence the render effect.
if (node.name === 'this') {
context.state.init.push(b.stmt(call));
context.state.init.push(statement);
} else {
const has_use =
parent.type === 'RegularElement' && parent.attributes.find((a) => a.type === 'UseDirective');
if (has_use) {
context.state.init.push(b.stmt(b.call('$.effect', b.thunk(call))));
if (defer) {
context.state.init.push(statement);
} else {
context.state.after_update.push(b.stmt(call));
context.state.after_update.push(statement);
}
}
}

@ -1,4 +1,4 @@
/** @import { CallExpression, Expression, MemberExpression } from 'estree' */
/** @import { CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
@ -62,6 +62,17 @@ export function CallExpression(node, context) {
is_ignored(node, 'state_snapshot_uncloneable') && b.true
);
case '$effect':
case '$effect.pre': {
const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect';
const func = /** @type {Expression} */ (context.visit(node.arguments[0]));
const expr = b.call(callee, /** @type {Expression} */ (func));
expr.callee.loc = node.callee.loc; // ensure correct mapping
return expr;
}
case '$effect.root':
return b.call(
'$.effect_root',
@ -69,7 +80,7 @@ export function CallExpression(node, context) {
);
case '$effect.pending':
return b.call('$.pending');
return b.call('$.eager', b.thunk(b.call('$.pending')));
case '$inspect':
case '$inspect().with':

@ -312,9 +312,10 @@ export function EachBlock(node, context) {
declarations.push(b.let(node.index, index));
}
const { has_await } = node.metadata.expression;
const get_collection = b.thunk(collection, has_await);
const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const is_async = node.metadata.expression.is_async();
const get_collection = b.thunk(collection, node.metadata.expression.has_await);
const thunk = is_async ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const render_args = [b.id('$$anchor'), item];
if (uses_index || collection_id) render_args.push(index);
@ -341,12 +342,13 @@ export function EachBlock(node, context) {
statements.unshift(b.stmt(b.call('$.validate_each_keys', thunk, key_function)));
}
if (has_await) {
if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([get_collection]),
b.arrow([context.state.node, b.id('$$collection')], b.block(statements))
)

@ -1,4 +1,4 @@
/** @import { Expression, ExpressionStatement } from 'estree' */
/** @import { ExpressionStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
@ -11,16 +11,6 @@ export function ExpressionStatement(node, context) {
if (node.expression.type === 'CallExpression') {
const rune = get_rune(node.expression, context.state.scope);
if (rune === '$effect' || rune === '$effect.pre') {
const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect';
const func = /** @type {Expression} */ (context.visit(node.expression.arguments[0]));
const expr = b.call(callee, /** @type {Expression} */ (func));
expr.callee.loc = node.expression.callee.loc; // ensure correct mapping
return b.stmt(expr);
}
if (rune === '$inspect.trace') {
return b.empty;
}

@ -1,7 +1,5 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_hoisted_params } from '../utils.js';
import * as b from '#compiler/builders';
/**
* @param {FunctionDeclaration} node
@ -10,14 +8,5 @@ import * as b from '#compiler/builders';
export function FunctionDeclaration(node, context) {
const state = { ...context.state, in_constructor: false, in_derived: false };
if (node.metadata?.hoisted === true) {
const params = build_hoisted_params(node, context);
const body = context.visit(node.body, state);
context.state.hoisted.push(/** @type {FunctionDeclaration} */ ({ ...node, params, body }));
return b.empty;
}
context.next(state);
}

@ -11,9 +11,10 @@ import { build_expression } from './shared/utils.js';
export function HtmlTag(node, context) {
context.state.template.push_comment();
const { has_await } = node.metadata.expression;
const is_async = node.metadata.expression.is_async();
const expression = build_expression(context, node.expression, node.metadata.expression);
const html = has_await ? b.call('$.get', b.id('$$html')) : expression;
const html = is_async ? b.call('$.get', b.id('$$html')) : expression;
const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
@ -30,13 +31,14 @@ export function HtmlTag(node, context) {
);
// push into init, so that bindings run afterwards, which might trigger another run and override hydration
if (node.metadata.expression.has_await) {
if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$html')], b.block([statement]))
)
)

@ -25,9 +25,10 @@ export function IfBlock(node, context) {
statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate)));
}
const { has_await } = node.metadata.expression;
const is_async = node.metadata.expression.is_async();
const expression = build_expression(context, node.test, node.metadata.expression);
const test = has_await ? b.call('$.get', b.id('$$condition')) : expression;
const test = is_async ? b.call('$.get', b.id('$$condition')) : expression;
/** @type {Expression[]} */
const args = [
@ -71,13 +72,14 @@ export function IfBlock(node, context) {
statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
if (has_await) {
if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$condition')], b.block(statements))
)
)

@ -1,16 +0,0 @@
/** @import { ImportDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
/**
* @param {ImportDeclaration} node
* @param {ComponentContext} context
*/
export function ImportDeclaration(node, context) {
if ('hoisted' in context.state) {
context.state.hoisted.push(node);
return b.empty;
}
context.next();
}

@ -11,10 +11,10 @@ import { build_expression, add_svelte_meta } from './shared/utils.js';
export function KeyBlock(node, context) {
context.state.template.push_comment();
const { has_await } = node.metadata.expression;
const is_async = node.metadata.expression.is_async();
const expression = build_expression(context, node.expression, node.metadata.expression);
const key = b.thunk(has_await ? b.call('$.get', b.id('$$key')) : expression);
const key = b.thunk(is_async ? b.call('$.get', b.id('$$key')) : expression);
const body = /** @type {Expression} */ (context.visit(node.fragment));
let statement = add_svelte_meta(
@ -23,12 +23,13 @@ export function KeyBlock(node, context) {
'key'
);
if (has_await) {
if (is_async) {
statement = b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$key')], b.block([statement]))
)
);

@ -1,14 +1,15 @@
/** @import { Expression, ImportDeclaration, MemberExpression, Program } from 'estree' */
/** @import { Expression, ImportDeclaration, MemberExpression, Node, Program } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_getter, is_prop_source } from '../utils.js';
import * as b from '#compiler/builders';
import { add_state_transformers } from './shared/declarations.js';
import { transform_body } from '../../shared/transform-async.js';
/**
* @param {Program} _
* @param {Program} node
* @param {ComponentContext} context
*/
export function Program(_, context) {
export function Program(node, context) {
if (!context.state.analysis.runes) {
context.state.transform['$$props'] = {
read: (node) => ({ ...node, name: '$$sanitized_props' })
@ -137,5 +138,16 @@ export function Program(_, context) {
add_state_transformers(context);
if (context.state.is_instance) {
return {
...node,
body: transform_body(
context.state.analysis.instance_body,
b.id('$.run'),
(node) => /** @type {Node} */ (context.visit(node))
)
};
}
context.next();
}

@ -11,7 +11,7 @@ import {
import { is_ignored } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_attribute, is_custom_element_node } from '../../../nodes.js';
import { create_attribute, ExpressionMetadata, is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js';
import {
@ -267,10 +267,7 @@ export function RegularElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) =>
metadata.has_call || metadata.has_await
? context.state.memoizer.add(value, metadata.has_await)
: value
(value, metadata) => context.state.memoizer.add(value, metadata)
);
const update = build_element_attribute_update(node, node_id, name, value, attributes);
@ -487,11 +484,25 @@ function setup_select_synchronization(value_binding, context) {
);
}
/**
* @param {ExpressionMetadata} target
* @param {ExpressionMetadata} source
*/
function merge_metadata(target, source) {
target.has_assignment ||= source.has_assignment;
target.has_await ||= source.has_await;
target.has_call ||= source.has_call;
target.has_member_expression ||= source.has_member_expression;
target.has_state ||= source.has_state;
for (const r of source.references) target.references.add(r);
for (const b of source.dependencies) target.dependencies.add(b);
}
/**
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @param {Memoizer} memoizer
* @return {ObjectExpression | Identifier}
*/
export function build_class_directives_object(
class_directives,
@ -499,26 +510,25 @@ export function build_class_directives_object(
memoizer = context.state.memoizer
) {
let properties = [];
let has_call_or_state = false;
let has_await = false;
const metadata = new ExpressionMetadata();
for (const d of class_directives) {
merge_metadata(metadata, d.metadata.expression);
const expression = /** @type Expression */ (context.visit(d.expression));
properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
has_await ||= d.metadata.expression.has_await;
}
const directives = b.object(properties);
return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives;
return memoizer.add(directives, metadata);
}
/**
* @param {AST.StyleDirective[]} style_directives
* @param {ComponentContext} context
* @param {Memoizer} memoizer
* @return {ObjectExpression | ArrayExpression | Identifier}}
*/
export function build_style_directives_object(
style_directives,
@ -528,10 +538,11 @@ export function build_style_directives_object(
const normal = b.object([]);
const important = b.object([]);
let has_call_or_state = false;
let has_await = false;
const metadata = new ExpressionMetadata();
for (const d of style_directives) {
merge_metadata(metadata, d.metadata.expression);
const expression =
d.value === true
? build_getter(b.id(d.name), context.state)
@ -539,14 +550,11 @@ export function build_style_directives_object(
const object = d.modifiers.includes('important') ? important : normal;
object.properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
has_await ||= d.metadata.expression.has_await;
}
const directives = important.properties.length ? b.array([normal, important]) : normal;
return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives;
return memoizer.add(directives, metadata);
}
/**
@ -675,7 +683,7 @@ function build_element_special_value_attribute(
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value
state.memoizer.add(value, metadata)
);
const evaluated = context.state.scope.evaluate(value);

@ -22,11 +22,11 @@ export function RenderTag(node, context) {
for (let i = 0; i < call.arguments.length; i++) {
const arg = /** @type {Expression} */ (call.arguments[i]);
const metadata = node.metadata.arguments[i];
let expression = build_expression(context, arg, metadata);
const memoized = memoizer.add(expression, metadata);
if (metadata.has_await || metadata.has_call) {
expression = b.call('$.get', memoizer.add(expression, metadata.has_await));
if (expression !== memoized) {
expression = b.call('$.get', memoized);
}
args.push(b.thunk(expression));
@ -71,13 +71,15 @@ export function RenderTag(node, context) {
}
const async_values = memoizer.async_values();
const blockers = memoizer.blockers();
if (async_values) {
if (async_values || blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
blockers,
memoizer.async_values(),
b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements))
)

@ -35,7 +35,7 @@ export function SlotElement(node, context) {
context,
(value, metadata) =>
metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata.has_await))
? b.call('$.get', memoizer.add(value, metadata))
: value
);
@ -74,13 +74,15 @@ export function SlotElement(node, context) {
);
const async_values = memoizer.async_values();
const blockers = memoizer.blockers();
if (async_values) {
if (async_values || blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
blockers,
async_values,
b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements))
)

@ -93,10 +93,10 @@ export function SvelteElement(node, context) {
);
}
const { has_await } = node.metadata.expression;
const is_async = node.metadata.expression.is_async();
const expression = /** @type {Expression} */ (context.visit(node.tag));
const get_tag = b.thunk(has_await ? b.call('$.get', b.id('$$tag')) : expression);
const get_tag = b.thunk(is_async ? b.call('$.get', b.id('$$tag')) : expression);
/** @type {Statement[]} */
const inner = inner_context.state.init;
@ -139,13 +139,14 @@ export function SvelteElement(node, context) {
)
);
if (has_await) {
if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$tag')], b.block(statements))
)
)

@ -2,6 +2,8 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { hash } from '../../../../../utils.js';
import { filename } from '../../../../state.js';
/**
* @param {AST.SvelteHead} node
@ -13,6 +15,7 @@ export function SvelteHead(node, context) {
b.stmt(
b.call(
'$.head',
b.literal(hash(filename)),
b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)))
)
)

@ -1,16 +1,19 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_template_chunk } from './shared/utils.js';
import { build_template_chunk, Memoizer } from './shared/utils.js';
/**
* @param {AST.TitleElement} node
* @param {ComponentContext} context
*/
export function TitleElement(node, context) {
const memoizer = new Memoizer();
const { has_state, value } = build_template_chunk(
/** @type {any} */ (node.fragment.nodes),
context
context,
context.state,
(value, metadata) => memoizer.add(value, metadata)
);
const evaluated = context.state.scope.evaluate(value);
@ -26,9 +29,21 @@ export function TitleElement(node, context) {
)
);
// Always in an $effect so it only changes the title once async work is done
if (has_state) {
context.state.update.push(statement);
context.state.after_update.push(
b.stmt(
b.call(
'$.template_effect',
b.arrow(memoizer.apply(), b.block([statement])),
memoizer.sync_values(),
memoizer.async_values(),
memoizer.blockers(),
b.true
)
)
);
} else {
context.state.init.push(statement);
context.state.after_update.push(b.stmt(b.call('$.effect', b.thunk(b.block([statement])))));
}
}

@ -7,7 +7,6 @@ import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
import { is_hoisted_function } from '../../utils.js';
import { get_value } from './shared/declarations.js';
/**
@ -32,13 +31,6 @@ export function VariableDeclaration(node, context) {
rune === '$state.snapshot' ||
rune === '$host'
) {
if (init != null && is_hoisted_function(init)) {
context.state.hoisted.push(
b.const(declarator.id, /** @type {Expression} */ (context.visit(init)))
);
continue;
}
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}
@ -201,18 +193,25 @@ export function VariableDeclaration(node, context) {
/** @type {CallExpression} */ (init)
);
// for now, only wrap async derived in $.save if it's not
// a top-level instance derived. TODO in future maybe we
// can dewaterfall all of them?
const should_save = context.state.is_instance && context.state.scope.function_depth > 1;
if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ (context.visit(value));
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
/** @type {Expression} */
let call = b.call(
'$.async_derived',
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
call = save(call);
call = should_save ? save(call) : b.await(call);
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call));
@ -232,18 +231,22 @@ export function VariableDeclaration(node, context) {
if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') {
const id = b.id(context.state.scope.generate('$$d'));
/** @type {Expression} */
let call = b.call('$.derived', rune === '$derived' ? b.thunk(expression) : expression);
rhs = b.call('$.get', id);
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
call = b.call(
'$.async_derived',
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
call = save(call);
call = should_save ? save(call) : b.await(call);
}
if (dev) {
@ -295,16 +298,6 @@ export function VariableDeclaration(node, context) {
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (!has_state && !has_props) {
const init = declarator.init;
if (init != null && is_hoisted_function(init)) {
context.state.hoisted.push(
b.const(declarator.id, /** @type {Expression} */ (context.visit(init)))
);
continue;
}
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}

@ -16,12 +16,12 @@ import { determine_slot } from '../../../../../utils/slot.js';
* @returns {Statement}
*/
export function build_component(node, component_name, context) {
/**
* @type {Expression}
*/
/** @type {Expression} */
const anchor = context.state.node;
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {Array<() => void>} */
const delayed_props = [];
@ -129,14 +129,16 @@ export function build_component(node, component_name, context) {
(events[attribute.name] ||= []).push(handler);
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
const memoized_expression = memoizer.add(expression, attribute.metadata.expression);
const is_memoized = expression !== memoized_expression;
if (attribute.metadata.expression.has_state || attribute.metadata.expression.has_await) {
if (
is_memoized ||
attribute.metadata.expression.has_state ||
attribute.metadata.expression.has_await
) {
props_and_spreads.push(
b.thunk(
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call
? b.call('$.get', memoizer.add(expression, attribute.metadata.expression.has_await))
: expression
)
b.thunk(is_memoized ? b.call('$.get', memoized_expression) : expression)
);
} else {
props_and_spreads.push(expression);
@ -147,10 +149,10 @@ export function build_component(node, component_name, context) {
b.init(
attribute.name,
build_attribute_value(attribute.value, context, (value, metadata) => {
const memoized = memoizer.add(value, metadata);
// TODO put the derived in the local block
return metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
return value !== memoized ? b.call('$.get', memoized) : value;
}).value
)
);
@ -184,9 +186,9 @@ export function build_component(node, component_name, context) {
);
});
return should_wrap_in_derived
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
const memoized = memoizer.add(value, metadata, should_wrap_in_derived);
return value !== memoized ? b.call('$.get', memoized) : value;
}
);
@ -497,12 +499,14 @@ export function build_component(node, component_name, context) {
memoizer.apply();
const async_values = memoizer.async_values();
const blockers = memoizer.blockers();
if (async_values) {
if (async_values || blockers) {
return b.stmt(
b.call(
'$.async',
anchor,
blockers,
async_values,
b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements))
)

@ -1,5 +1,5 @@
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { escape_html } from '../../../../../../escaping.js';
import { normalize_attribute } from '../../../../../../utils.js';
@ -8,6 +8,7 @@ import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js';
import { build_expression, build_template_chunk, Memoizer } from './utils.js';
import { ExpressionMetadata } from '../../../../nodes.js';
/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
@ -35,7 +36,7 @@ export function build_attribute_effect(
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call || metadata.has_await ? memoizer.add(value, metadata.has_await) : value
memoizer.add(value, metadata)
);
if (
@ -52,9 +53,7 @@ export function build_attribute_effect(
} else {
let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call || attribute.metadata.expression.has_await) {
value = memoizer.add(value, attribute.metadata.expression.has_await);
}
value = memoizer.add(value, attribute.metadata.expression);
values.push(b.spread(value));
}
@ -90,6 +89,7 @@ export function build_attribute_effect(
b.arrow(ids, b.object(values)),
memoizer.sync_values(),
memoizer.async_values(),
memoizer.blockers(),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
@ -155,9 +155,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
value = b.call('$.clsx', value);
}
return metadata.has_call || metadata.has_await
? context.state.memoizer.add(value, metadata.has_await)
: value;
return context.state.memoizer.add(value, metadata);
});
/** @type {Identifier | undefined} */
@ -166,7 +164,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
/** @type {ObjectExpression | Identifier | undefined} */
let prev;
/** @type {ObjectExpression | Identifier | undefined} */
/** @type {Expression | undefined} */
let next;
if (class_directives.length) {
@ -227,7 +225,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
*/
export function build_set_style(node_id, attribute, style_directives, context) {
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? context.state.memoizer.add(value, metadata.has_await) : value
context.state.memoizer.add(value, metadata)
);
/** @type {Identifier | undefined} */

@ -1,9 +1,10 @@
/** @import { Expression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { is_capture_event, is_passive_event } from '../../../../../../utils.js';
import { dev, locator } from '../../../../../state.js';
import * as b from '#compiler/builders';
import { ExpressionMetadata } from '../../../../nodes.js';
/**
* @param {AST.Attribute} node
@ -26,40 +27,12 @@ export function visit_event_attribute(node, context) {
let handler = build_event_handler(tag.expression, tag.metadata.expression, context);
if (node.metadata.delegated) {
let delegated_assignment;
if (!context.state.events.has(event_name)) {
context.state.events.add(event_name);
}
// Hoist function if we can, otherwise we leave the function as is
if (node.metadata.delegated.hoisted) {
if (node.metadata.delegated.function === tag.expression) {
const func_name = context.state.scope.root.unique('on_' + event_name);
context.state.hoisted.push(b.var(func_name, handler));
handler = func_name;
}
const hoisted_params = /** @type {Expression[]} */ (
node.metadata.delegated.function.metadata.hoisted_params
);
// When we hoist a function we assign an array with the function and all
// hoisted closure params.
if (hoisted_params) {
const args = [handler, ...hoisted_params];
delegated_assignment = b.array(args);
} else {
delegated_assignment = handler;
}
} else {
delegated_assignment = handler;
}
context.state.init.push(
b.stmt(
b.assignment('=', b.member(context.state.node, '__' + event_name), delegated_assignment)
)
b.stmt(b.assignment('=', b.member(context.state.node, '__' + event_name), handler))
);
} else {
const statement = b.stmt(

@ -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.has_await)
!(node.body.metadata.has_await || node.metadata.expression.is_async())
) {
node.metadata.is_controlled = true;
} else {

@ -1,14 +1,11 @@
/** @import { ArrowFunctionExpression, FunctionExpression, Node } from 'estree' */
/** @import { ComponentContext } from '../../types' */
import { build_hoisted_params } from '../../utils.js';
/**
* @param {ArrowFunctionExpression | FunctionExpression} node
* @param {ComponentContext} context
*/
export const visit_function = (node, context) => {
const metadata = node.metadata;
let state = { ...context.state, in_constructor: false, in_derived: false };
if (node.type === 'FunctionExpression') {
@ -16,15 +13,5 @@ export const visit_function = (node, context) => {
state.in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor';
}
if (metadata?.hoisted === true) {
const params = build_hoisted_params(node, context);
return /** @type {FunctionExpression} */ ({
...node,
params,
body: context.visit(node.body, state)
});
}
context.next(state);
};

@ -1,5 +1,5 @@
/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
@ -9,6 +9,7 @@ import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
import { build_getter } from '../../utils.js';
import { ExpressionMetadata } from '../../../../nodes.js';
/**
* A utility for extracting complex expressions (such as call expressions)
@ -21,14 +22,32 @@ export class Memoizer {
/** @type {Array<{ id: Identifier, expression: Expression }>} */
#async = [];
/** @type {Set<Expression>} */
#blockers = new Set();
/**
* @param {Expression} expression
* @param {boolean} has_await
* @param {ExpressionMetadata} metadata
* @param {boolean} memoize_if_state
*/
add(expression, has_await) {
add(expression, metadata, memoize_if_state = false) {
for (const binding of metadata.dependencies) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}
}
const should_memoize =
metadata.has_call || metadata.has_await || (memoize_if_state && metadata.has_state);
if (!should_memoize) {
// no memoization required
return expression;
}
const id = b.id('#'); // filled in later
(has_await ? this.#async : this.#sync).push({ id, expression });
(metadata.has_await ? this.#async : this.#sync).push({ id, expression });
return id;
}
@ -40,6 +59,10 @@ export class Memoizer {
});
}
blockers() {
return this.#blockers.size > 0 ? b.array([...this.#blockers]) : undefined;
}
deriveds(runes = true) {
return this.#sync.map((memo) =>
b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression)))
@ -72,8 +95,7 @@ export function build_template_chunk(
values,
context,
state = context.state,
memoize = (value, metadata) =>
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value
memoize = (value, metadata) => state.memoizer.add(value, metadata)
) {
/** @type {Expression[]} */
const expressions = [];
@ -176,7 +198,8 @@ export function build_render_statement(state) {
: b.block(state.update)
),
memoizer.sync_values(),
memoizer.async_values()
memoizer.async_values(),
memoizer.blockers()
)
);
}

@ -1,4 +1,4 @@
/** @import { Program, Property, Statement, VariableDeclarator } from 'estree' */
/** @import * as ESTree from 'estree' */
/** @import { AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentServerTransformState, ComponentVisitors, ServerTransformState, Visitors } from './types.js' */
/** @import { Analysis, ComponentAnalysis } from '../../types.js' */
@ -25,6 +25,7 @@ import { IfBlock } from './visitors/IfBlock.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { Program } from './visitors/Program.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
@ -40,7 +41,7 @@ import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { call_component_renderer, create_async_block } from './visitors/shared/utils.js';
import { call_component_renderer } from './visitors/shared/utils.js';
/** @type {Visitors} */
const global_visitors = {
@ -53,6 +54,7 @@ const global_visitors = {
Identifier,
LabeledStatement,
MemberExpression,
Program,
PropertyDefinition,
UpdateExpression,
VariableDeclaration
@ -86,7 +88,7 @@ const template_visitors = {
/**
* @param {ComponentAnalysis} analysis
* @param {ValidatedCompileOptions} options
* @returns {Program}
* @returns {ESTree.Program}
*/
export function server_component(analysis, options) {
/** @type {ComponentServerTransformState} */
@ -95,7 +97,7 @@ export function server_component(analysis, options) {
options,
scope: analysis.module.scope,
scopes: analysis.module.scopes,
hoisted: [b.import_all('$', 'svelte/internal/server')],
hoisted: [b.import_all('$', 'svelte/internal/server'), ...analysis.instance_body.hoisted],
legacy_reactive_statements: new Map(),
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
@ -103,17 +105,18 @@ export function server_component(analysis, options) {
namespace: options.namespace,
preserve_whitespace: options.preserveWhitespace,
state_fields: new Map(),
skip_hydration_boundaries: false
skip_hydration_boundaries: false,
is_instance: false
};
const module = /** @type {Program} */ (
const module = /** @type {ESTree.Program} */ (
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
);
const instance = /** @type {Program} */ (
const instance = /** @type {ESTree.Program} */ (
walk(
/** @type {AST.SvelteNode} */ (analysis.instance.ast),
{ ...state, scopes: analysis.instance.scopes },
{ ...state, scopes: analysis.instance.scopes, is_instance: true },
{
...global_visitors,
ImportDeclaration(node) {
@ -131,7 +134,7 @@ export function server_component(analysis, options) {
)
);
const template = /** @type {Program} */ (
const template = /** @type {ESTree.Program} */ (
walk(
/** @type {AST.SvelteNode} */ (analysis.template.ast),
{ ...state, scopes: analysis.template.scopes },
@ -140,7 +143,7 @@ export function server_component(analysis, options) {
)
);
/** @type {VariableDeclarator[]} */
/** @type {ESTree.VariableDeclarator[]} */
const legacy_reactive_declarations = [];
for (const [node] of analysis.reactive_statements) {
@ -192,7 +195,7 @@ export function server_component(analysis, options) {
b.function_declaration(
b.id('$$render_inner'),
[b.id('$$renderer')],
b.block(/** @type {Statement[]} */ (rest))
b.block(/** @type {ESTree.Statement[]} */ (rest))
),
b.do_while(
b.unary('!', b.id('$$settled')),
@ -219,7 +222,7 @@ export function server_component(analysis, options) {
// Propagate values of bound props upwards if they're undefined in the parent and have a value.
// Don't do this as part of the props retrieval because people could eagerly mutate the prop in the instance script.
/** @type {Property[]} */
/** @type {ESTree.Property[]} */
const props = [];
for (const [name, binding] of analysis.instance.scope.declarations) {
@ -239,14 +242,10 @@ export function server_component(analysis, options) {
}
let component_block = b.block([
.../** @type {Statement[]} */ (instance.body),
.../** @type {Statement[]} */ (template.body)
.../** @type {ESTree.Statement[]} */ (instance.body),
.../** @type {ESTree.Statement[]} */ (template.body)
]);
if (analysis.instance.has_await) {
component_block = b.block([create_async_block(component_block)]);
}
// trick esrap into including comments
component_block.loc = instance.loc;
@ -395,7 +394,7 @@ export function server_component(analysis, options) {
/**
* @param {Analysis} analysis
* @param {ValidatedModuleCompileOptions} options
* @returns {Program}
* @returns {ESTree.Program}
*/
export function server_module(analysis, options) {
/** @type {ServerTransformState} */
@ -408,10 +407,11 @@ export function server_module(analysis, options) {
// to be present for `javascript_visitors_legacy` and so is included in module
// transform state as well as component transform state
legacy_reactive_statements: new Map(),
state_fields: new Map()
state_fields: new Map(),
is_instance: false
};
const module = /** @type {Program} */ (
const module = /** @type {ESTree.Program} */ (
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
);

@ -25,8 +25,12 @@ export function AwaitBlock(node, context) {
)
);
if (node.metadata.expression.has_await) {
statement = create_async_block(b.block([statement]));
if (node.metadata.expression.is_async()) {
statement = create_async_block(
b.block([statement]),
node.metadata.expression.blockers(),
node.metadata.expression.has_await
);
}
context.state.template.push(statement, block_close);

@ -12,7 +12,14 @@ import { get_inspect_args } from '../../utils.js';
export function CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
if (
rune === '$host' ||
rune === '$effect' ||
rune === '$effect.pre' ||
rune === '$inspect.trace'
) {
// we will only encounter `$effect` etc if they are top-level statements in the <script>
// following an `await`, otherwise they are removed by the ExpressionStatement visitor
return b.void0;
}

@ -34,7 +34,11 @@ export function EachBlock(node, context) {
const new_body = /** @type {BlockStatement} */ (context.visit(node.body)).body;
each.push(...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_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)
);
const for_loop = b.for(
b.declaration('let', [
@ -65,8 +69,15 @@ export function EachBlock(node, context) {
block.body.push(for_loop);
}
if (node.metadata.expression.has_await) {
state.template.push(create_async_block(block), block_close);
if (node.metadata.expression.is_async()) {
state.template.push(
create_async_block(
block,
node.metadata.expression.blockers(),
node.metadata.expression.has_await
),
block_close
);
} else {
state.template.push(...block.body, block_close);
}

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { create_push } from './shared/utils.js';
/**
* @param {AST.HtmlTag} node
@ -10,9 +11,6 @@ import * as b from '#compiler/builders';
export function HtmlTag(node, context) {
const expression = /** @type {Expression} */ (context.visit(node.expression));
const call = b.call('$.html', expression);
context.state.template.push(
node.metadata.expression.has_await
? b.stmt(b.call('$$renderer.push', b.thunk(call, true)))
: call
);
context.state.template.push(create_push(call, node.metadata.expression, true));
}

@ -23,12 +23,20 @@ export function IfBlock(node, context) {
/** @type {Statement} */
let statement = b.if(test, consequent, alternate);
if (
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
) {
statement = create_async_block(b.block([statement]));
node.alternate?.metadata.has_await;
if (is_async || has_await) {
statement = create_async_block(
b.block([statement]),
node.metadata.expression.blockers(),
!!has_await
);
}
context.state.template.push(statement, block_close);

@ -1,16 +1,22 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { empty_comment } from './shared/utils.js';
import { block_close, block_open, empty_comment } from './shared/utils.js';
/**
* @param {AST.KeyBlock} node
* @param {ComponentContext} context
*/
export function KeyBlock(node, context) {
const is_async = node.metadata.expression.is_async();
if (is_async) context.state.template.push(block_open);
context.state.template.push(
empty_comment,
/** @type {BlockStatement} */ (context.visit(node.fragment)),
empty_comment
);
if (is_async) context.state.template.push(block_close);
}

@ -0,0 +1,25 @@
/** @import { Node, Program } from 'estree' */
/** @import { Context, ComponentServerTransformState } from '../types' */
import * as b from '#compiler/builders';
import { transform_body } from '../../shared/transform-async.js';
/**
* @param {Program} node
* @param {Context} context
*/
export function Program(node, context) {
if (context.state.is_instance) {
const state = /** @type {ComponentServerTransformState} */ (context.state);
return {
...node,
body: transform_body(
state.analysis.instance_body,
b.id('$$renderer.run'),
(node) => /** @type {Node} */ (context.visit(node))
)
};
}
context.next();
}

@ -12,7 +12,8 @@ import {
process_children,
build_template,
create_child_block,
PromiseOptimiser
PromiseOptimiser,
create_async_block
} from './shared/utils.js';
/**
@ -202,13 +203,19 @@ export function RegularElement(node, context) {
state.template.push(b.stmt(b.call('$.pop_element')));
}
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
)
if (optimiser.is_async()) {
let statement = create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
);
const blockers = optimiser.blockers();
if (blockers.elements.length > 0) {
statement = create_async_block(b.block([statement]), blockers, false, false);
}
context.state.template.push(statement);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);

@ -3,32 +3,48 @@
/** @import { ComponentContext } from '../types.js' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { empty_comment } from './shared/utils.js';
import { create_async_block, empty_comment, PromiseOptimiser } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
* @param {ComponentContext} context
*/
export function RenderTag(node, context) {
const optimiser = new PromiseOptimiser();
const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments;
const snippet_function = /** @type {Expression} */ (context.visit(callee));
const snippet_function = optimiser.transform(
/** @type {Expression} */ (context.visit(callee)),
node.metadata.expression
);
const snippet_args = raw_args.map((arg) => {
return /** @type {Expression} */ (context.visit(arg));
const snippet_args = raw_args.map((arg, i) => {
return optimiser.transform(
/** @type {Expression} */ (context.visit(arg)),
node.metadata.arguments[i]
);
});
context.state.template.push(
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
b.id('$$renderer'),
...snippet_args
)
let statement = b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
b.id('$$renderer'),
...snippet_args
)
);
if (optimiser.is_async()) {
statement = create_async_block(
b.block([optimiser.apply(), statement]),
optimiser.blockers(),
optimiser.has_await
);
}
context.state.template.push(statement);
if (!context.state.skip_hydration_boundaries) {
context.state.template.push(empty_comment);
}

@ -65,10 +65,13 @@ export function SlotElement(node, context) {
fallback
);
const statement =
optimiser.expressions.length > 0
? create_async_block(b.block([optimiser.apply(), b.stmt(slot)]))
: b.stmt(slot);
const statement = optimiser.is_async()
? create_async_block(
b.block([optimiser.apply(), b.stmt(slot)]),
optimiser.blockers(),
optimiser.has_await
)
: b.stmt(slot);
context.state.template.push(block_open, statement, block_close);
}

@ -6,7 +6,12 @@ import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.js';
import { build_template, create_child_block, PromiseOptimiser } from './shared/utils.js';
import {
build_template,
create_async_block,
create_child_block,
PromiseOptimiser
} from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@ -39,11 +44,14 @@ export function SvelteElement(node, context) {
const optimiser = new PromiseOptimiser();
/** @type {Statement[]} */
let statements = [];
build_element_attributes(node, { ...context, state }, optimiser.transform);
if (dev) {
const location = /** @type {Location} */ (locator(node.start));
context.state.template.push(
statements.push(
b.stmt(
b.call(
'$.push_element',
@ -74,9 +82,21 @@ export function SvelteElement(node, context) {
statement = create_child_block(b.block([optimiser.apply(), statement]), true);
}
context.state.template.push(statement);
statements.push(statement);
if (dev) {
context.state.template.push(b.stmt(b.call('$.pop_element')));
statements.push(b.stmt(b.call('$.pop_element')));
}
if (node.metadata.expression.is_async()) {
statements = [
create_async_block(
b.block(statements),
node.metadata.expression.blockers(),
node.metadata.expression.has_await
)
];
}
context.state.template.push(...statements);
}

@ -2,6 +2,8 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { hash } from '../../../../../utils.js';
import { filename } from '../../../../state.js';
/**
* @param {AST.SvelteHead} node
@ -11,6 +13,13 @@ export function SvelteHead(node, context) {
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push(
b.stmt(b.call('$.head', b.id('$$renderer'), b.arrow([b.id('$$renderer')], block)))
b.stmt(
b.call(
'$.head',
b.literal(hash(filename)),
b.id('$$renderer'),
b.arrow([b.id('$$renderer')], block)
)
)
);
}

@ -5,8 +5,7 @@ import {
empty_comment,
build_attribute_value,
create_async_block,
PromiseOptimiser,
build_template
PromiseOptimiser
} from './utils.js';
import * as b from '#compiler/builders';
import { is_element_node } from '../../../../nodes.js';
@ -323,8 +322,14 @@ export function build_inline_component(node, expression, context) {
);
}
if (optimiser.expressions.length > 0) {
statement = create_async_block(b.block([optimiser.apply(), statement]));
const is_async = optimiser.is_async();
if (is_async) {
statement = create_async_block(
b.block([optimiser.apply(), statement]),
optimiser.blockers(),
optimiser.has_await
);
}
if (dynamic && custom_css_props.length === 0) {
@ -334,6 +339,7 @@ export function build_inline_component(node, expression, context) {
context.state.template.push(statement);
if (
!is_async &&
!context.state.skip_hydration_boundaries &&
custom_css_props.length === 0 &&
optimiser.expressions.length === 0

@ -1,13 +1,9 @@
/** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { binding_properties } from '../../../../bindings.js';
import {
create_attribute,
create_expression_metadata,
is_custom_element_node
} from '../../../../nodes.js';
import { create_attribute, ExpressionMetadata, is_custom_element_node } from '../../../../nodes.js';
import { regex_starts_with_newline } from '../../../../patterns.js';
import * as b from '#compiler/builders';
import {
@ -160,7 +156,7 @@ export function build_element_attributes(node, context, transform) {
build_attribute_value(value_attribute.value, context, transform)
),
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
}
])
@ -174,7 +170,7 @@ export function build_element_attributes(node, context, transform) {
end: -1,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
}
])

@ -1,5 +1,5 @@
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { Expression, Identifier, Node, Statement, BlockStatement, ArrayExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { escape_html } from '../../../../../../escaping.js';
@ -13,6 +13,7 @@ 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_expression } from '../../../../../utils/ast.js';
import { ExpressionMetadata } from '../../../../nodes.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);
@ -76,12 +77,11 @@ export function process_children(nodes, { visit, state }) {
}
for (const node of nodes) {
if (node.type === 'ExpressionTag' && node.metadata.expression.has_await) {
if (node.type === 'ExpressionTag' && node.metadata.expression.is_async()) {
flush();
const visited = /** @type {Expression} */ (visit(node.expression));
state.template.push(
b.stmt(b.call('$$renderer.push', b.thunk(b.call('$.escape', visited), true)))
);
const expression = /** @type {Expression} */ (visit(node.expression));
state.template.push(create_push(b.call('$.escape', expression), node.metadata.expression));
} else if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') {
sequence.push(node);
} else {
@ -274,9 +274,50 @@ export function create_child_block(body, async) {
/**
* Creates a `$$renderer.async(...)` expression statement
* @param {BlockStatement | Expression} body
* @param {ArrayExpression} blockers
* @param {boolean} has_await
* @param {boolean} needs_hydration_markers
*/
export function create_async_block(
body,
blockers = b.array([]),
has_await = true,
needs_hydration_markers = true
) {
return b.stmt(
b.call(
needs_hydration_markers ? '$$renderer.async_block' : '$$renderer.async',
blockers,
b.arrow([b.id('$$renderer')], body, has_await)
)
);
}
/**
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
* @param {boolean} needs_hydration_markers
* @returns {Expression | Statement}
*/
export function create_async_block(body) {
return b.stmt(b.call('$$renderer.async', b.arrow([b.id('$$renderer')], body, true)));
export function create_push(expression, metadata, needs_hydration_markers = false) {
if (metadata.is_async()) {
let statement = b.stmt(b.call('$$renderer.push', b.thunk(expression, metadata.has_await)));
const blockers = metadata.blockers();
if (blockers.elements.length > 0) {
statement = create_async_block(
b.block([statement]),
blockers,
false,
needs_hydration_markers
);
}
return statement;
}
return expression;
}
/**
@ -290,17 +331,36 @@ export function call_component_renderer(body, component_fn_id) {
);
}
/**
* A utility for optimising promises in templates. Without it code like
* `<Component foo={await fetch()} bar={await other()} />` would be transformed
* into two blocking promises, with it it's using `Promise.all` to await them.
* It also keeps track of blocking promises, i.e. those that need to be resolved before continuing.
*/
export class PromiseOptimiser {
/** @type {Expression[]} */
expressions = [];
has_await = false;
/** @type {Set<Expression>} */
#blockers = new Set();
/**
*
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
*/
transform = (expression, metadata) => {
for (const binding of metadata.dependencies) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}
}
if (metadata.has_await) {
this.has_await = true;
const length = this.expressions.push(expression);
return b.id(`$$${length - 1}`);
}
@ -309,6 +369,10 @@ export class PromiseOptimiser {
};
apply() {
if (this.expressions.length === 0) {
return b.empty;
}
if (this.expressions.length === 1) {
return b.const('$$0', this.expressions[0]);
}
@ -326,4 +390,12 @@ export class PromiseOptimiser {
b.await(b.call('Promise.all', promises))
);
}
blockers() {
return b.array([...this.#blockers]);
}
is_async() {
return this.expressions.length > 0 || this.#blockers.size > 0;
}
}

@ -0,0 +1,102 @@
/** @import * as ESTree from 'estree' */
/** @import { ComponentAnalysis } from '../../types' */
import * as b from '#compiler/builders';
/**
* Transforms the body of the instance script in such a way that await expressions are made non-blocking as much as possible.
*
* Example Transformation:
* ```js
* let x = 1;
* let data = await fetch('/api');
* let y = data.value;
* ```
* becomes:
* ```js
* let x = 1;
* var data, y;
* var $$promises = $.run([
* () => data = await fetch('/api'),
* () => y = data.value
* ]);
* ```
* where `$$promises` is an array of promises that are resolved in the order they are declared,
* and which expressions in the template can await on like `await $$promises[0]` which means they
* wouldn't have to wait for e.g. `$$promises[1]` to resolve.
*
* @param {ComponentAnalysis['instance_body']} instance_body
* @param {ESTree.Expression} runner
* @param {(node: ESTree.Node) => ESTree.Node} transform
* @returns {Array<ESTree.Statement | ESTree.VariableDeclaration>}
*/
export function transform_body(instance_body, runner, transform) {
// Any sync statements before the first await expression
const statements = instance_body.sync.map(
(node) => /** @type {ESTree.Statement | ESTree.VariableDeclaration} */ (transform(node))
);
// Declarations for the await expressions (they will asign to them; need to be hoisted to be available in whole instance scope)
if (instance_body.declarations.length > 0) {
statements.push(
b.declaration(
'var',
instance_body.declarations.map((id) => b.declarator(id))
)
);
}
// Thunks for the await expressions
if (instance_body.async.length > 0) {
const thunks = instance_body.async.map((s) => {
if (s.node.type === 'VariableDeclarator') {
const visited = /** @type {ESTree.VariableDeclaration} */ (
transform(b.var(s.node.id, s.node.init))
);
if (visited.declarations.length === 1) {
return b.thunk(
b.assignment('=', visited.declarations[0].id, visited.declarations[0].init ?? b.void0),
s.has_await
);
}
// if we have multiple declarations, it indicates destructuring
return b.thunk(
b.block([
b.var(visited.declarations[0].id, visited.declarations[0].init),
...visited.declarations
.slice(1)
.map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0)))
]),
s.has_await
);
}
if (s.node.type === 'ClassDeclaration') {
return b.thunk(
b.assignment(
'=',
s.node.id,
/** @type {ESTree.ClassExpression} */ ({ ...s.node, type: 'ClassExpression' })
),
s.has_await
);
}
if (s.node.type === 'ExpressionStatement') {
const expression = /** @type {ESTree.Expression} */ (transform(s.node.expression));
return expression.type === 'AwaitExpression'
? b.thunk(expression, true)
: b.thunk(b.unary('void', expression), s.has_await);
}
return b.thunk(b.block([/** @type {ESTree.Statement} */ (transform(s.node))]), s.has_await);
});
// TODO get the `$$promises` ID from scope
statements.push(b.var('$$promises', b.call(runner, b.array(thunks))));
}
return statements;
}

@ -8,5 +8,8 @@ export interface TransformState {
readonly scope: Scope;
readonly scopes: Map<AST.SvelteNode, Scope>;
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;
readonly state_fields: Map<string, StateField>;
}

@ -1,36 +1,17 @@
/** @import { Context } from 'zimmerframe' */
/** @import { TransformState } from './types.js' */
/** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */
/** @import { Node, Expression, CallExpression, MemberExpression } from 'estree' */
import {
regex_ends_with_whitespaces,
regex_not_whitespace,
regex_starts_with_newline,
regex_starts_with_whitespaces
} from '../patterns.js';
import * as b from '#compiler/builders';
import * as e from '../../errors.js';
import { walk } from 'zimmerframe';
import { extract_identifiers } from '../../utils/ast.js';
import check_graph_for_cycles from '../2-analyze/utils/check_graph_for_cycles.js';
import is_reference from 'is-reference';
import { set_scope } from '../scope.js';
import { dev } from '../../state.js';
/**
* @param {Node} node
* @returns {boolean}
*/
export function is_hoisted_function(node) {
if (
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' ||
node.type === 'FunctionDeclaration'
) {
return node.metadata?.hoisted === true;
}
return false;
}
/**
* Match Svelte 4 behaviour by sorting ConstTag nodes in topological order

@ -1,5 +1,6 @@
/** @import { Expression, PrivateIdentifier } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { AST, Binding } from '#compiler' */
import * as b from '#compiler/builders';
/**
* All nodes that can appear elsewhere than the top level, have attributes and can contain children
@ -59,25 +60,61 @@ export function create_attribute(name, start, end, value) {
name,
value,
metadata: {
delegated: null,
delegated: false,
needs_clsx: false
}
};
}
export class ExpressionMetadata {
/** True if the expression references state directly, or _might_ (via member/call expressions) */
has_state = false;
/**
* @returns {ExpressionMetadata}
*/
export function create_expression_metadata() {
return {
dependencies: new Set(),
references: new Set(),
has_state: false,
has_call: false,
has_member_expression: false,
has_assignment: false,
has_await: false
};
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call = false;
/** True if the expression contains `await` */
has_await = false;
/** True if the expression includes a member expression */
has_member_expression = false;
/** True if the expression includes an assignment or an update */
has_assignment = false;
/**
* All the bindings that are referenced eagerly (not inside functions) in this expression
* @type {Set<Binding>}
*/
dependencies = new Set();
/**
* True if the expression references state directly, or _might_ (via member/call expressions)
* @type {Set<Binding>}
*/
references = new Set();
/** @type {null | Set<Expression>} */
#blockers = null;
#get_blockers() {
if (!this.#blockers) {
this.#blockers = new Set();
for (const d of this.dependencies) {
if (d.blocker) this.#blockers.add(d.blocker);
}
}
return this.#blockers;
}
blockers() {
return b.array([...this.#get_blockers()]);
}
is_async() {
return this.has_await || this.#get_blockers().size > 0;
}
}
/**

@ -3,7 +3,7 @@
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import { create_expression_metadata } from './nodes.js';
import { ExpressionMetadata } from './nodes.js';
import * as b from '#compiler/builders';
import * as e from '../errors.js';
import {
@ -108,6 +108,9 @@ export class Binding {
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
references = [];
/** @type {Array<{ value: Expression; scope: Scope }>} */
assignments = [];
/**
* For `legacy_reactive`: its reactive dependencies
* @type {Binding[]}
@ -129,6 +132,15 @@ export class Binding {
mutated = false;
reassigned = false;
/**
* 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
* TODO the blocker is set during transform which feels a bit grubby
* @type {Expression | null}
*/
blocker = null;
/**
*
* @param {Scope} scope
@ -143,6 +155,10 @@ export class Binding {
this.initial = initial;
this.kind = kind;
this.declaration_kind = declaration_kind;
if (initial) {
this.assignments.push({ value: /** @type {Expression} */ (initial), scope });
}
}
get updated() {
@ -859,7 +875,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */
const references = [];
/** @type {[Scope, Pattern | MemberExpression][]} */
/** @type {[Scope, Pattern | MemberExpression, Expression][]} */
const updates = [];
/**
@ -1047,12 +1063,13 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
// updates
AssignmentExpression(node, { state, next }) {
updates.push([state.scope, node.left]);
updates.push([state.scope, node.left, node.right]);
next();
},
UpdateExpression(node, { state, next }) {
updates.push([state.scope, /** @type {Identifier | MemberExpression} */ (node.argument)]);
const expression = /** @type {Identifier | MemberExpression} */ (node.argument);
updates.push([state.scope, expression, expression]);
next();
},
@ -1201,7 +1218,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (node.fallback) visit(node.fallback, { scope });
node.metadata = {
expression: create_expression_metadata(),
expression: new ExpressionMetadata(),
keyed: false,
contains_group_binding: false,
index: scope.root.unique('$$index'),
@ -1273,10 +1290,11 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
},
BindDirective(node, context) {
updates.push([
context.state.scope,
/** @type {Identifier | MemberExpression} */ (node.expression)
]);
if (node.expression.type !== 'SequenceExpression') {
const expression = /** @type {Identifier | MemberExpression} */ (node.expression);
updates.push([context.state.scope, expression, expression]);
}
context.next();
},
@ -1311,7 +1329,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
scope.reference(node, path);
}
for (const [scope, node] of updates) {
for (const [scope, node, value] of updates) {
for (const expression of unwrap_pattern(node)) {
const left = object(expression);
const binding = left && scope.get(left.name);
@ -1319,6 +1337,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (binding !== null && left !== binding.node) {
if (left === expression) {
binding.reassigned = true;
binding.assignments.push({ value, scope });
} else {
binding.mutated = true;
}

@ -3,11 +3,19 @@ import type {
AwaitExpression,
CallExpression,
ClassBody,
ClassDeclaration,
FunctionDeclaration,
Identifier,
LabeledStatement,
Program
ModuleDeclaration,
Pattern,
Program,
Statement,
VariableDeclaration,
VariableDeclarator
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
import type { ExpressionMetadata } from './nodes.js';
export interface Js {
ast: Program;
@ -27,6 +35,14 @@ export interface ReactiveStatement {
dependencies: Binding[];
}
export interface AwaitedDeclaration {
id: Identifier;
has_await: boolean;
pattern: Pattern;
metadata: ExpressionMetadata;
updated_by: Set<Identifier>;
}
/**
* Analysis common to modules and components
*/
@ -108,30 +124,13 @@ export interface ComponentAnalysis extends Analysis {
* Every snippet that is declared locally
*/
snippets: Set<AST.SnippetBlock>;
}
declare module 'estree' {
interface ArrowFunctionExpression {
metadata: {
hoisted: boolean;
hoisted_params: Pattern[];
scope: Scope;
};
}
interface FunctionExpression {
metadata: {
hoisted: boolean;
hoisted_params: Pattern[];
scope: Scope;
};
}
interface FunctionDeclaration {
metadata: {
hoisted: boolean;
hoisted_params: Pattern[];
scope: Scope;
};
}
/**
* Pre-transformed `<script>` block
*/
instance_body: {
hoisted: Array<Statement | ModuleDeclaration>;
sync: Array<Statement | ModuleDeclaration | VariableDeclaration>;
async: Array<{ node: Statement | VariableDeclarator; has_await: boolean }>;
declarations: Array<Identifier>;
};
}

@ -294,23 +294,6 @@ export type DeclarationKind =
| 'using'
| 'await using';
export interface ExpressionMetadata {
/** All the bindings that are referenced eagerly (not inside functions) in this expression */
dependencies: Set<Binding>;
/** All the bindings that are referenced inside this expression, including inside functions */
references: Set<Binding>;
/** True if the expression references state directly, or _might_ (via member/call expressions) */
has_state: boolean;
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call: boolean;
/** True if the expression contains `await` */
has_await: boolean;
/** True if the expression includes a member expression */
has_member_expression: boolean;
/** True if the expression includes an assignment or an update */
has_assignment: boolean;
}
export interface StateField {
type: StateCreationRuneName;
node: PropertyDefinition | AssignmentExpression;

@ -1,12 +1,10 @@
import type { Binding, ExpressionMetadata } from '#compiler';
import type { Binding } from '#compiler';
import type {
ArrayExpression,
ArrowFunctionExpression,
VariableDeclaration,
VariableDeclarator,
Expression,
FunctionDeclaration,
FunctionExpression,
Identifier,
MemberExpression,
Node,
@ -19,6 +17,7 @@ import type {
} from 'estree';
import type { Scope } from '../phases/scope';
import type { _CSS } from './css';
import type { ExpressionMetadata } from '../phases/nodes';
/**
* - `html` the default, for e.g. `<div>` or `<span>`
@ -27,13 +26,6 @@ import type { _CSS } from './css';
*/
export type Namespace = 'html' | 'svg' | 'mathml';
export type DelegatedEvent =
| {
hoisted: true;
function: ArrowFunctionExpression | FunctionExpression | FunctionDeclaration;
}
| { hoisted: false };
export namespace AST {
export interface BaseNode {
type: string;
@ -56,6 +48,7 @@ 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;
};
}
@ -214,6 +207,7 @@ export namespace AST {
expression: Identifier | MemberExpression | SequenceExpression;
/** @internal */
metadata: {
binding?: Binding | null;
binding_group_name: Identifier;
parent_each_blocks: EachBlock[];
};
@ -531,7 +525,7 @@ export namespace AST {
/** @internal */
metadata: {
/** May be set if this is an event attribute */
delegated: null | DelegatedEvent;
delegated: boolean;
/** May be `true` if this is a `class` attribute that needs `clsx` */
needs_clsx: boolean;
};

@ -1,4 +1,4 @@
/** @import { AST } from '#compiler' */
/** @import { AST, Scope } from '#compiler' */
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '#compiler/builders';

@ -42,8 +42,7 @@ export function arrow(params, body, async = false) {
body,
expression: body.type !== 'BlockStatement',
generator: false,
async,
metadata: /** @type {any} */ (null) // should not be used by codegen
async
};
}
@ -83,6 +82,17 @@ export function block(body) {
return { type: 'BlockStatement', body };
}
/**
* @param {ESTree.Identifier | null} id
* @param {ESTree.ClassBody} body
* @param {ESTree.Expression | null} [superClass]
* @param {ESTree.Decorator[]} [decorators]
* @returns {ESTree.ClassExpression}
*/
export function class_expression(id, body, superClass, decorators = []) {
return { type: 'ClassExpression', body, superClass, decorators };
}
/**
* @param {string} name
* @param {ESTree.Statement} body
@ -185,7 +195,7 @@ export function declaration(kind, declarations) {
/**
* @param {ESTree.Pattern | string} pattern
* @param {ESTree.Expression} [init]
* @param {ESTree.Expression | null} [init]
* @returns {ESTree.VariableDeclarator}
*/
export function declarator(pattern, init) {
@ -237,8 +247,7 @@ export function function_declaration(id, params, body, async = false) {
params,
body,
generator: false,
async,
metadata: /** @type {any} */ (null) // should not be used by codegen
async
};
}
@ -522,7 +531,7 @@ const this_instance = {
/**
* @param {string | ESTree.Pattern} pattern
* @param { ESTree.Expression} [init]
* @param {ESTree.Expression | null} [init]
* @returns {ESTree.VariableDeclaration}
*/
function let_builder(pattern, init) {
@ -531,7 +540,7 @@ function let_builder(pattern, init) {
/**
* @param {string | ESTree.Pattern} pattern
* @param { ESTree.Expression} init
* @param {ESTree.Expression | null} init
* @returns {ESTree.VariableDeclaration}
*/
function const_builder(pattern, init) {
@ -540,7 +549,7 @@ function const_builder(pattern, init) {
/**
* @param {string | ESTree.Pattern} pattern
* @param { ESTree.Expression} [init]
* @param {ESTree.Expression | null} [init]
* @returns {ESTree.VariableDeclaration}
*/
function var_builder(pattern, init) {
@ -595,8 +604,7 @@ function function_builder(id, params, body, async = false) {
params,
body,
generator: false,
async,
metadata: /** @type {any} */ (null) // should not be used by codegen
async
};
}

@ -6,6 +6,13 @@ export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6;
export const BOUNDARY_EFFECT = 1 << 7;
/**
* Indicates that a reaction is connected to an effect root either it is an effect,
* or it is a derived that is depended on by at least one effect. If a derived has
* no dependents, we can disconnect it from the graph, allowing it to either be
* GC'd or reconnected later if an effect comes to depend on it again
*/
export const CONNECTED = 1 << 9;
export const CLEAN = 1 << 10;
export const DIRTY = 1 << 11;
export const MAYBE_DIRTY = 1 << 12;
@ -13,6 +20,7 @@ export const INERT = 1 << 13;
export const DESTROYED = 1 << 14;
// Flags exclusive to effects
/** Set once an effect that should run synchronously has run */
export const EFFECT_RAN = 1 << 15;
/**
* 'Transparent' effects do not create a transition boundary.
@ -25,8 +33,6 @@ export const EFFECT_PRESERVED = 1 << 19;
export const USER_EFFECT = 1 << 20;
// Flags exclusive to deriveds
export const UNOWNED = 1 << 8;
export const DISCONNECTED = 1 << 9;
/**
* Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary

@ -128,7 +128,11 @@ export function setContext(key, context) {
if (async_mode_flag) {
var flags = /** @type {Effect} */ (active_effect).f;
var valid = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0;
var valid =
!active_reaction &&
(flags & BRANCH_EFFECT) !== 0 &&
// pop() runs synchronously, so this indicates we're setting context after an await
!(/** @type {ComponentContext} */ (component_context).i);
if (!valid) {
e.set_context_after_init();
@ -173,6 +177,7 @@ export function getAllContexts() {
export function push(props, runes = false, fn) {
component_context = {
p: component_context,
i: false,
c: null,
e: null,
s: props,
@ -208,6 +213,8 @@ export function pop(component) {
context.x = component;
}
context.i = true;
component_context = context.p;
if (DEV) {

@ -33,8 +33,17 @@ export function inspect(get_value, inspector, show_stack = false) {
inspector(...snap);
if (!initial) {
const stack = get_stack('$inspect(...)');
// eslint-disable-next-line no-console
console.log(get_stack('UpdatedAt'));
if (stack) {
// eslint-disable-next-line no-console
console.groupCollapsed('stack trace');
// eslint-disable-next-line no-console
console.log(stack);
// eslint-disable-next-line no-console
console.groupEnd();
}
}
} else {
inspector(initial ? 'init' : 'update', ...snap);

@ -179,8 +179,7 @@ export function get_stack(label) {
});
define_property(error, 'name', {
// 'Error' suffix is required for stack traces to be rendered properly
value: `${label}Error`
value: label
});
return /** @type {Error & { stack: string }} */ (error);

@ -14,10 +14,11 @@ import { get_boundary } from './boundary.js';
/**
* @param {TemplateNode} node
* @param {Array<Promise<void>>} blockers
* @param {Array<() => Promise<any>>} expressions
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
*/
export function async(node, expressions, fn) {
export function async(node, blockers = [], expressions = [], fn) {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = !boundary.is_pending();
@ -35,7 +36,7 @@ export function async(node, expressions, fn) {
set_hydrate_node(end);
}
flatten([], expressions, (values) => {
flatten(blockers, [], expressions, (values) => {
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
@ -47,12 +48,12 @@ export function async(node, expressions, fn) {
fn(node, ...values);
} finally {
if (was_hydrating) {
set_hydrating(false);
}
boundary.update_pending_count(-1);
batch.decrement(blocking);
}
if (was_hydrating) {
set_hydrating(false);
}
});
}

@ -12,7 +12,7 @@ import {
import { queue_micro_task } from '../task.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import { is_runes } from '../../context.js';
import { flushSync, is_flushing_sync } from '../../reactivity/batch.js';
import { Batch, flushSync, is_flushing_sync } from '../../reactivity/batch.js';
import { BranchManager } from './branches.js';
import { capture, unset_context } from '../../reactivity/async.js';
@ -69,7 +69,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
if (destroyed) return;
resolved = true;
restore();
// We don't want to restore the previous batch here; {#await} blocks don't follow the async logic
// we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak.
restore(false);
// Make sure we have a batch, since the branch manager expects one to exist
Batch.ensure();
if (hydrating) {
// `restore()` could set `hydrating` to `true`, which we very much

@ -34,7 +34,7 @@ import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch, effect_pending_updates } from '../../reactivity/batch.js';
import { Batch } 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';
@ -110,12 +110,6 @@ export class Boundary {
*/
#effect_pending = null;
#effect_pending_update = () => {
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#local_pending_count);
}
};
#effect_pending_subscriber = createSubscriber(() => {
this.#effect_pending = source(this.#local_pending_count);
@ -329,7 +323,10 @@ export class Boundary {
this.#update_pending_count(d);
this.#local_pending_count += d;
effect_pending_updates.add(this.#effect_pending_update);
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#local_pending_count);
}
}
get_effect_pending() {

@ -3,22 +3,13 @@ import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hyd
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants';
import { HYDRATION_START } from '../../../../constants.js';
/**
* @type {Node | undefined}
*/
let head_anchor;
export function reset_head_anchor() {
head_anchor = undefined;
}
/**
* @param {string} hash
* @param {(anchor: Node) => void} render_fn
* @returns {void}
*/
export function head(render_fn) {
export function head(hash, render_fn) {
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
// therefore we need to skip that when we detect that we're not in hydration mode.
let previous_hydrate_node = null;
@ -30,15 +21,13 @@ export function head(render_fn) {
if (hydrating) {
previous_hydrate_node = hydrate_node;
// There might be multiple head blocks in our app, so we need to account for each one needing independent hydration.
if (head_anchor === undefined) {
head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head));
}
var head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head));
// There might be multiple head blocks in our app, and they could have been
// rendered in an arbitrary order — find one corresponding to this component
while (
head_anchor !== null &&
(head_anchor.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
(head_anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (head_anchor).data !== hash)
) {
head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
}
@ -48,7 +37,10 @@ export function head(render_fn) {
if (head_anchor === null) {
set_hydrating(false);
} else {
head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (get_next_sibling(head_anchor)));
var start = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
head_anchor.remove(); // in case this component is repeated
set_hydrate_node(start);
}
}
@ -61,7 +53,6 @@ export function head(render_fn) {
} finally {
if (was_hydrating) {
set_hydrating(true);
head_anchor = hydrate_node; // so that next head block starts from the correct node
set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node));
}
}

@ -7,7 +7,7 @@ import { add_form_reset_listener, autofocus } from './misc.js';
import * as w from '../../warnings.js';
import { LOADING_ATTR_SYMBOL } from '#client/constants';
import { queue_micro_task } from '../task.js';
import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js';
import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js';
import {
active_effect,
active_reaction,
@ -378,7 +378,7 @@ function set_attributes(
const opts = {};
const event_handle_key = '$$' + key;
let event_name = key.slice(2);
var delegated = is_delegated(event_name);
var delegated = can_delegate_event(event_name);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
@ -483,6 +483,7 @@ function set_attributes(
* @param {(...expressions: any) => Record<string | symbol, any>} fn
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {Array<Promise<void>>} blockers
* @param {string} [css_hash]
* @param {boolean} [should_remove_defaults]
* @param {boolean} [skip_warning]
@ -492,11 +493,12 @@ export function attribute_effect(
fn,
sync = [],
async = [],
blockers = [],
css_hash,
should_remove_defaults = false,
skip_warning = false
) {
flatten(sync, async, (values) => {
flatten(blockers, sync, async, (values) => {
/** @type {Record<string | symbol, any> | undefined} */
var prev = undefined;

@ -1,5 +1,5 @@
import { teardown } from '../../reactivity/effects.js';
import { define_property, is_array } from '../../../shared/utils.js';
import { define_property } from '../../../shared/utils.js';
import { hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { FILENAME } from '../../../../constants.js';
@ -258,12 +258,7 @@ export function handle_event_propagation(event) {
// -> the target could not have been disabled because it emits the event in the first place
event.target === current_target)
) {
if (is_array(delegated)) {
var [fn, ...data] = delegated;
fn.apply(current_target, [event, ...data]);
} else {
delegated.call(current_target, event);
}
delegated.call(current_target, event);
}
} catch (error) {
if (throw_error) {

@ -29,7 +29,7 @@ export function handle_error(error) {
// if the error occurred while creating this subtree, we let it
// bubble up until it hits a boundary that can handle it
if ((effect.f & BOUNDARY_EFFECT) === 0) {
if (!effect.parent && error instanceof Error) {
if (DEV && !effect.parent && error instanceof Error) {
apply_adjustments(error);
}
@ -61,7 +61,7 @@ export function invoke_error_boundary(error, effect) {
effect = effect.parent;
}
if (error instanceof Error) {
if (DEV && error instanceof Error) {
apply_adjustments(error);
}

@ -262,12 +262,12 @@ export function flush_sync_in_effect() {
}
/**
* Cannot commit a fork that was already committed or discarded
* Cannot commit a fork that was already discarded
* @returns {never}
*/
export function fork_discarded() {
if (DEV) {
const error = new Error(`fork_discarded\nCannot commit a fork that was already committed or discarded\nhttps://svelte.dev/e/fork_discarded`);
const error = new Error(`fork_discarded\nCannot commit a fork that was already discarded\nhttps://svelte.dev/e/fork_discarded`);
error.name = 'Svelte error';

@ -100,6 +100,7 @@ export {
export {
async_body,
for_await_track_reactivity_loss,
run,
save,
track_reactivity_loss
} from './reactivity/async.js';

@ -53,7 +53,7 @@ export function proxy(value) {
var is_proxied_array = is_array(value);
var version = source(0);
var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null;
var stack = DEV && tracing_mode_flag ? get_stack('created at') : null;
var parent_version = update_version;
/**
@ -267,7 +267,7 @@ export function proxy(value) {
if (other_s !== undefined) {
set(other_s, UNINITIALIZED);
} else if (i in target) {
// If the item exists in the original, we need to create a uninitialized source,
// If the item exists in the original, we need to create an uninitialized source,
// else a later read of the property would result in a source being created with
// the value of the original item at that index.
other_s = with_parent(() => source(UNINITIALIZED, stack));

@ -1,5 +1,5 @@
/** @import { Effect, TemplateNode, Value } from '#client' */
import { DESTROYED } from '#client/constants';
import { DESTROYED, STALE_REACTION } from '#client/constants';
import { DEV } from 'esm-env';
import {
component_context,
@ -25,26 +25,18 @@ import {
set_from_async_derived
} from './deriveds.js';
import { aborted } from './effects.js';
import {
hydrate_next,
hydrate_node,
hydrating,
set_hydrate_node,
set_hydrating,
skip_nodes
} from '../dom/hydration.js';
import { create_text } from '../dom/operations.js';
import { hydrate_next, hydrating, set_hydrate_node, skip_nodes } from '../dom/hydration.js';
/**
*
* @param {Array<Promise<void>>} blockers
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {(values: Value[]) => any} fn
*/
export function flatten(sync, async, fn) {
export function flatten(blockers, sync, async, fn) {
const d = is_runes() ? derived : derived_safe_equal;
if (async.length === 0) {
if (async.length === 0 && blockers.length === 0) {
fn(sync.map(d));
return;
}
@ -54,31 +46,42 @@ export function flatten(sync, async, fn) {
var restore = capture();
var was_hydrating = hydrating;
function run() {
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => {
restore();
try {
fn([...sync.map(d), ...result]);
} catch (error) {
// ignore errors in blocks that have already been destroyed
if ((parent.f & DESTROYED) === 0) {
invoke_error_boundary(error, parent);
}
}
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => {
batch?.deactivate();
unset_context();
})
.catch((error) => {
invoke_error_boundary(error, parent);
});
}
if (blockers.length > 0) {
Promise.all(blockers).then(() => {
restore();
try {
fn([...sync.map(d), ...result]);
} catch (error) {
// ignore errors in blocks that have already been destroyed
if ((parent.f & DESTROYED) === 0) {
invoke_error_boundary(error, parent);
}
return run();
} finally {
batch?.deactivate();
unset_context();
}
if (was_hydrating) {
set_hydrating(false);
}
batch?.deactivate();
unset_context();
})
.catch((error) => {
invoke_error_boundary(error, parent);
});
} else {
run();
}
}
/**
@ -92,26 +95,15 @@ export function capture() {
var previous_component_context = component_context;
var previous_batch = current_batch;
var was_hydrating = hydrating;
if (was_hydrating) {
var previous_hydrate_node = hydrate_node;
}
if (DEV) {
var previous_dev_stack = dev_stack;
}
return function restore() {
return function restore(activate_batch = true) {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_component_context);
previous_batch?.activate();
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
}
if (activate_batch) previous_batch?.activate();
if (DEV) {
set_from_async_derived(null);
@ -249,13 +241,79 @@ export async function async_body(anchor, fn) {
invoke_error_boundary(error, active);
}
} finally {
if (was_hydrating) {
set_hydrating(false);
}
boundary.update_pending_count(-1);
batch.decrement(blocking);
unset_context();
}
}
/**
* @param {Array<() => void | Promise<void>>} thunks
*/
export function run(thunks) {
const restore = capture();
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = !boundary.is_pending();
boundary.update_pending_count(1);
batch.increment(blocking);
var active = /** @type {Effect} */ (active_effect);
/** @type {null | { error: any }} */
var errored = null;
/** @param {any} error */
const handle_error = (error) => {
errored = { error }; // wrap in object in case a promise rejects with a falsy value
if (!aborted(active)) {
invoke_error_boundary(error, active);
}
};
var promise = Promise.resolve(thunks[0]()).catch(handle_error);
var promises = [promise];
for (const fn of thunks.slice(1)) {
promise = promise
.then(() => {
if (errored) {
throw errored.error;
}
if (aborted(active)) {
throw STALE_REACTION;
}
try {
restore();
return fn();
} finally {
// TODO do we need it here as well as below?
unset_context();
}
})
.catch(handle_error)
.finally(() => {
unset_context();
});
promises.push(promise);
}
promise
// wait one more tick, so that template effects are
// guaranteed to run before `$effect(...)`
.then(() => Promise.resolve())
.finally(() => {
boundary.update_pending_count(-1);
batch.decrement(blocking);
});
return promises;
}

@ -14,33 +14,26 @@ import {
MAYBE_DIRTY,
DERIVED,
BOUNDARY_EFFECT,
EAGER_EFFECT
EAGER_EFFECT,
HEAD_EFFECT,
ERROR_VALUE
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
import {
active_effect,
get,
increment_write_version,
is_dirty,
is_updating_effect,
set_is_updating_effect,
set_signal_status,
tick,
update_effect
} from '../runtime.js';
import * as e from '../errors.js';
import { flush_tasks, queue_micro_task } from '../dom/task.js';
import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
import {
flush_eager_effects,
eager_effects,
old_values,
set_eager_effects,
source,
update
} from './sources.js';
import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js';
import { eager_effect, unlink_effect } from './effects.js';
/**
@ -74,9 +67,6 @@ export let previous_batch = null;
*/
export let batch_values = null;
/** @type {Set<() => void>} */
export let effect_pending_updates = new Set();
/** @type {Effect[]} */
let queued_root_effects = [];
@ -196,6 +186,8 @@ export class Batch {
flush_queued_effects(target.effects);
previous_batch = null;
this.#deferred?.resolve();
}
batch_values = null;
@ -294,12 +286,16 @@ export class Batch {
this.previous.set(source, value);
}
this.current.set(source, source.v);
batch_values?.set(source, source.v);
// Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
if ((source.f & ERROR_VALUE) === 0) {
this.current.set(source, source.v);
batch_values?.set(source, source.v);
}
}
activate() {
current_batch = this;
this.apply();
}
deactivate() {
@ -322,16 +318,6 @@ export class Batch {
}
this.deactivate();
for (const update of effect_pending_updates) {
effect_pending_updates.delete(update);
update();
if (current_batch !== null) {
// only do one at a time
break;
}
}
}
discard() {
@ -432,8 +418,6 @@ export class Batch {
this.committed = true;
batches.delete(this);
this.#deferred?.resolve();
}
/**
@ -513,7 +497,7 @@ export class Batch {
}
apply() {
if (!async_mode_flag || batches.size === 1) return;
if (!async_mode_flag || (!this.is_fork && batches.size === 1)) return;
// if there are multiple batches, we are 'time travelling' —
// we need to override values with the ones in this batch...
@ -813,7 +797,12 @@ export function schedule_effect(signal) {
// if the effect is being scheduled because a parent (each/await/etc) block
// updated an internal source, bail out or we'll cause a second flush
if (is_flushing && effect === active_effect && (flags & BLOCK_EFFECT) !== 0) {
if (
is_flushing &&
effect === active_effect &&
(flags & BLOCK_EFFECT) !== 0 &&
(flags & HEAD_EFFECT) === 0
) {
return;
}
@ -913,28 +902,36 @@ export function fork(fn) {
e.fork_timing();
}
const batch = Batch.ensure();
var batch = Batch.ensure();
batch.is_fork = true;
const settled = batch.settled();
var committed = false;
var settled = batch.settled();
flushSync(fn);
// revert state changes
for (const [source, value] of batch.previous) {
for (var [source, value] of batch.previous) {
source.v = value;
}
return {
commit: async () => {
if (committed) {
await settled;
return;
}
if (!batches.has(batch)) {
e.fork_discarded();
}
committed = true;
batch.is_fork = false;
// apply changes
for (const [source, value] of batch.current) {
for (var [source, value] of batch.current) {
source.v = value;
}
@ -945,9 +942,9 @@ export function fork(fn) {
// TODO maybe there's a better implementation?
flushSync(() => {
/** @type {Set<Effect>} */
const eager_effects = new Set();
var eager_effects = new Set();
for (const source of batch.current.keys()) {
for (var source of batch.current.keys()) {
mark_eager_effects(source, eager_effects);
}
@ -959,7 +956,7 @@ export function fork(fn) {
await settled;
},
discard: () => {
if (batches.has(batch)) {
if (!committed && batches.has(batch)) {
batches.delete(batch);
batch.discard();
}

@ -9,15 +9,14 @@ import {
EFFECT_PRESERVED,
MAYBE_DIRTY,
STALE_REACTION,
UNOWNED,
ASYNC,
WAS_MARKED
WAS_MARKED,
CONNECTED
} from '#client/constants';
import {
active_reaction,
active_effect,
set_signal_status,
skip_reaction,
update_reaction,
increment_write_version,
set_active_effect,
@ -27,7 +26,7 @@ import {
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
import * as w from '../warnings.js';
import { async_effect, destroy_effect, teardown } from './effects.js';
import { async_effect, destroy_effect, effect_tracking, teardown } from './effects.js';
import { eager_effects, internal_set, set_eager_effects, source } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@ -61,9 +60,7 @@ export function derived(fn) {
? /** @type {Derived} */ (active_reaction)
: null;
if (active_effect === null || (parent_derived !== null && (parent_derived.f & UNOWNED) !== 0)) {
flags |= UNOWNED;
} else {
if (active_effect !== null) {
// Since deriveds are evaluated lazily, any effects created inside them are
// created too late to ensure that the parent effect is added to the tree
active_effect.f |= EFFECT_PRESERVED;
@ -86,7 +83,7 @@ export function derived(fn) {
};
if (DEV && tracing_mode_flag) {
signal.created = get_stack('CreatedAt');
signal.created = get_stack('created at');
}
return signal;
@ -368,12 +365,16 @@ export function update_derived(derived) {
return;
}
// During time traveling we don't want to reset the status so that
// traversal of the graph in the other batches still happens
if (batch_values !== null) {
batch_values.set(derived, derived.v);
// only cache the value if we're in a tracking context, otherwise we won't
// clear the cache in `mark_reactions` when dependencies are updated
if (effect_tracking()) {
batch_values.set(derived, derived.v);
}
} else {
var status =
(skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;
var status = (derived.f & CONNECTED) === 0 ? MAYBE_DIRTY : CLEAN;
set_signal_status(derived, status);
}
}

@ -25,7 +25,6 @@ import {
ROOT_EFFECT,
EFFECT_TRANSPARENT,
DERIVED,
UNOWNED,
CLEAN,
EAGER_EFFECT,
HEAD_EFFECT,
@ -33,7 +32,8 @@ import {
EFFECT_PRESERVED,
STALE_REACTION,
USER_EFFECT,
ASYNC
ASYNC,
CONNECTED
} from '#client/constants';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
@ -48,11 +48,11 @@ import { without_reactive_context } from '../dom/elements/bindings/shared.js';
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
*/
export function validate_effect(rune) {
if (active_effect === null && active_reaction === null) {
e.effect_orphan(rune);
}
if (active_effect === null) {
if (active_reaction === null) {
e.effect_orphan(rune);
}
if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0 && active_effect === null) {
e.effect_in_unowned_derived();
}
@ -103,7 +103,7 @@ function create_effect(type, fn, sync, push = true) {
deps: null,
nodes_start: null,
nodes_end: null,
f: type | DIRTY,
f: type | DIRTY | CONNECTED,
first: null,
fn,
last: null,
@ -365,10 +365,12 @@ export function render_effect(fn, flags = 0) {
* @param {(...expressions: any) => void | (() => void)} fn
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {Array<Promise<void>>} blockers
* @param {boolean} defer
*/
export function template_effect(fn, sync = [], async = []) {
flatten(sync, async, (values) => {
create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true);
export function template_effect(fn, sync = [], async = [], blockers = [], defer = false) {
flatten(blockers, sync, async, (values) => {
create_effect(defer ? EFFECT : RENDER_EFFECT, () => fn(...values.map(get)), true);
});
}

@ -23,18 +23,18 @@ import {
DIRTY,
BRANCH_EFFECT,
EAGER_EFFECT,
UNOWNED,
MAYBE_DIRTY,
BLOCK_EFFECT,
ROOT_EFFECT,
ASYNC,
WAS_MARKED
WAS_MARKED,
CONNECTED
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js';
import { Batch, eager_block_effects, schedule_effect } from './batch.js';
import { Batch, batch_values, eager_block_effects, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
@ -76,7 +76,7 @@ export function source(v, stack) {
};
if (DEV && tracing_mode_flag) {
signal.created = stack ?? get_stack('CreatedAt');
signal.created = stack ?? get_stack('created at');
signal.updated = null;
signal.set_during_effect = false;
signal.trace = null;
@ -186,7 +186,7 @@ export function internal_set(source, value) {
if (DEV) {
if (tracing_mode_flag || active_effect !== null) {
const error = get_stack('UpdatedAt');
const error = get_stack('updated at');
if (error !== null) {
source.updated ??= new Map();
@ -211,7 +211,8 @@ export function internal_set(source, value) {
if ((source.f & DIRTY) !== 0) {
execute_derived(/** @type {Derived} */ (source));
}
set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY);
set_signal_status(source, (source.f & CONNECTED) !== 0 ? CLEAN : MAYBE_DIRTY);
}
source.wv = increment_write_version();
@ -333,9 +334,17 @@ function mark_reactions(signal, status) {
}
if ((flags & DERIVED) !== 0) {
var derived = /** @type {Derived} */ (reaction);
batch_values?.delete(derived);
if ((flags & WAS_MARKED) === 0) {
reaction.f |= WAS_MARKED;
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
// Only connected deriveds can be reliably unmarked right away
if (flags & CONNECTED) {
reaction.f |= WAS_MARKED;
}
mark_reactions(derived, MAYBE_DIRTY);
}
} else if (not_dirty) {
if ((flags & BLOCK_EFFECT) !== 0) {

@ -12,20 +12,13 @@ import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants
import { active_effect } from './runtime.js';
import { push, pop, component_context } from './context.js';
import { component_root } from './reactivity/effects.js';
import {
hydrate_next,
hydrate_node,
hydrating,
set_hydrate_node,
set_hydrating
} from './dom/hydration.js';
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from './dom/hydration.js';
import { array_from } from '../shared/utils.js';
import {
all_registered_events,
handle_event_propagation,
root_event_handles
} from './dom/elements/events.js';
import { reset_head_anchor } from './dom/blocks/svelte-head.js';
import * as w from './warnings.js';
import * as e from './errors.js';
import { assign_nodes } from './dom/template.js';
@ -152,7 +145,6 @@ export function hydrate(component, options) {
} finally {
set_hydrating(was_hydrating);
set_hydrate_node(previous_hydrate_node);
reset_head_anchor();
}
}

@ -4,6 +4,7 @@ import { get_descriptors, get_prototype_of, index_of } from '../shared/utils.js'
import {
destroy_block_effect_children,
destroy_effect_children,
effect_tracking,
execute_effect_teardown
} from './reactivity/effects.js';
import {
@ -11,13 +12,12 @@ import {
MAYBE_DIRTY,
CLEAN,
DERIVED,
UNOWNED,
DESTROYED,
BRANCH_EFFECT,
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT,
DISCONNECTED,
CONNECTED,
REACTION_IS_UPDATING,
STALE_REACTION,
ERROR_VALUE,
@ -137,10 +137,6 @@ export function set_update_version(value) {
update_version = value;
}
// If we are working with a get() chain that has no active container,
// to prevent memory leaks, we skip adding the reaction.
export let skip_reaction = false;
export function increment_write_version() {
return ++write_version;
}
@ -158,55 +154,18 @@ export function is_dirty(reaction) {
return true;
}
if (flags & DERIVED) {
reaction.f &= ~WAS_MARKED;
}
if ((flags & MAYBE_DIRTY) !== 0) {
var dependencies = reaction.deps;
var is_unowned = (flags & UNOWNED) !== 0;
if (flags & DERIVED) {
reaction.f &= ~WAS_MARKED;
}
if (dependencies !== null) {
var i;
var dependency;
var is_disconnected = (flags & DISCONNECTED) !== 0;
var is_unowned_connected = is_unowned && active_effect !== null && !skip_reaction;
var length = dependencies.length;
// If we are working with a disconnected or an unowned signal that is now connected (due to an active effect)
// then we need to re-connect the reaction to the dependency, unless the effect has already been destroyed
// (which can happen if the derived is read by an async derived)
if (
(is_disconnected || is_unowned_connected) &&
(active_effect === null || (active_effect.f & DESTROYED) === 0)
) {
var derived = /** @type {Derived} */ (reaction);
var parent = derived.parent;
for (i = 0; i < length; i++) {
dependency = dependencies[i];
// We always re-add all reactions (even duplicates) if the derived was
// previously disconnected, however we don't if it was unowned as we
// de-duplicate dependencies in that case
if (is_disconnected || !dependency?.reactions?.includes(derived)) {
(dependency.reactions ??= []).push(derived);
}
}
if (is_disconnected) {
derived.f ^= DISCONNECTED;
}
// If the unowned derived is now fully connected to the graph again (it's unowned and reconnected, has a parent
// and the parent is not unowned), then we can mark it as connected again, removing the need for the unowned
// flag
if (is_unowned_connected && parent !== null && (parent.f & UNOWNED) === 0) {
derived.f ^= UNOWNED;
}
}
for (i = 0; i < length; i++) {
dependency = dependencies[i];
for (var i = 0; i < length; i++) {
var dependency = dependencies[i];
if (is_dirty(/** @type {Derived} */ (dependency))) {
update_derived(/** @type {Derived} */ (dependency));
@ -218,9 +177,12 @@ export function is_dirty(reaction) {
}
}
// Unowned signals should never be marked as clean unless they
// are used within an active_effect without skip_reaction
if (!is_unowned || (active_effect !== null && !skip_reaction)) {
if (
(flags & CONNECTED) !== 0 &&
// During time traveling we don't want to reset the status so that
// traversal of the graph in the other batches still happens
batch_values === null
) {
set_signal_status(reaction, CLEAN);
}
}
@ -263,7 +225,6 @@ export function update_reaction(reaction) {
var previous_skipped_deps = skipped_deps;
var previous_untracked_writes = untracked_writes;
var previous_reaction = active_reaction;
var previous_skip_reaction = skip_reaction;
var previous_sources = current_sources;
var previous_component_context = component_context;
var previous_untracking = untracking;
@ -274,8 +235,6 @@ export function update_reaction(reaction) {
new_deps = /** @type {null | Value[]} */ (null);
skipped_deps = 0;
untracked_writes = null;
skip_reaction =
(flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null);
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
current_sources = null;
@ -311,12 +270,7 @@ export function update_reaction(reaction) {
reaction.deps = deps = new_deps;
}
if (
!skip_reaction ||
// Deriveds that already have reactions can cleanup, so we still add them as reactions
((flags & DERIVED) !== 0 &&
/** @type {import('#client').Derived} */ (reaction).reactions !== null)
) {
if (is_updating_effect && effect_tracking() && (reaction.f & CONNECTED) !== 0) {
for (i = skipped_deps; i < deps.length; i++) {
(deps[i].reactions ??= []).push(reaction);
}
@ -373,7 +327,6 @@ export function update_reaction(reaction) {
skipped_deps = previous_skipped_deps;
untracked_writes = previous_untracked_writes;
active_reaction = previous_reaction;
skip_reaction = previous_skip_reaction;
current_sources = previous_sources;
set_component_context(previous_component_context);
untracking = previous_untracking;
@ -415,9 +368,10 @@ function remove_reaction(signal, dependency) {
) {
set_signal_status(dependency, MAYBE_DIRTY);
// If we are working with a derived that is owned by an effect, then mark it as being
// disconnected.
if ((dependency.f & (UNOWNED | DISCONNECTED)) === 0) {
dependency.f ^= DISCONNECTED;
// disconnected and remove the mark flag, as it cannot be reliably removed otherwise
if ((dependency.f & CONNECTED) !== 0) {
dependency.f ^= CONNECTED;
dependency.f &= ~WAS_MARKED;
}
// Disconnect any reactions owned by this reaction
destroy_derived_effects(/** @type {Derived} **/ (dependency));
@ -564,10 +518,7 @@ export function get(signal) {
skipped_deps++;
} else if (new_deps === null) {
new_deps = [signal];
} else if (!skip_reaction || !new_deps.includes(signal)) {
// Normally we can push duplicated dependencies to `new_deps`, but if we're inside
// an unowned derived because skip_reaction is true, then we need to ensure that
// we don't have duplicates
} else if (!new_deps.includes(signal)) {
new_deps.push(signal);
}
}
@ -585,35 +536,22 @@ export function get(signal) {
}
}
}
} else if (
is_derived &&
/** @type {Derived} */ (signal).deps === null &&
/** @type {Derived} */ (signal).effects === null
) {
var derived = /** @type {Derived} */ (signal);
var parent = derived.parent;
if (parent !== null && (parent.f & UNOWNED) === 0) {
// If the derived is owned by another derived then mark it as unowned
// as the derived value might have been referenced in a different context
// since and thus its parent might not be its true owner anymore
derived.f ^= UNOWNED;
}
}
if (DEV) {
if (current_async_effect) {
var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0;
var was_read = current_async_effect.deps?.includes(signal);
// TODO reinstate this, but make it actually work
// if (current_async_effect) {
// var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0;
// var was_read = current_async_effect.deps?.includes(signal);
if (!tracking && !untracking && !was_read) {
w.await_reactivity_loss(/** @type {string} */ (signal.label));
// if (!tracking && !untracking && !was_read) {
// w.await_reactivity_loss(/** @type {string} */ (signal.label));
var trace = get_stack('TracedAt');
// eslint-disable-next-line no-console
if (trace) console.warn(trace);
}
}
// var trace = get_stack('traced at');
// // eslint-disable-next-line no-console
// if (trace) console.warn(trace);
// }
// }
recent_async_deriveds.delete(signal);
@ -628,7 +566,7 @@ export function get(signal) {
if (signal.trace) {
signal.trace();
} else {
trace = get_stack('TracedAt');
var trace = get_stack('traced at');
if (trace) {
var entry = tracing_expressions.entries.get(signal);
@ -656,7 +594,7 @@ export function get(signal) {
}
if (is_derived) {
derived = /** @type {Derived} */ (signal);
var derived = /** @type {Derived} */ (signal);
var value = derived.v;
@ -683,9 +621,11 @@ export function get(signal) {
if (is_dirty(derived)) {
update_derived(derived);
}
}
if (batch_values?.has(signal)) {
if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) {
reconnect(derived);
}
} else if (batch_values?.has(signal)) {
return batch_values.get(signal);
}
@ -696,6 +636,25 @@ export function get(signal) {
return signal.v;
}
/**
* (Re)connect a disconnected derived, so that it is notified
* of changes in `mark_reactions`
* @param {Derived} derived
*/
function reconnect(derived) {
if (derived.deps === null) return;
derived.f ^= CONNECTED;
for (const dep of derived.deps) {
(dep.reactions ??= []).push(derived);
if ((dep.f & DERIVED) !== 0 && (dep.f & CONNECTED) === 0) {
reconnect(/** @type {Derived} */ (dep));
}
}
}
/** @param {Derived} derived */
function depends_on_old_values(derived) {
if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst
@ -762,12 +721,12 @@ export function set_signal_status(signal, status) {
}
/**
* @param {Record<string, unknown>} obj
* @param {string[]} keys
* @returns {Record<string, unknown>}
* @param {Record<string | symbol, unknown>} obj
* @param {Array<string | symbol>} keys
* @returns {Record<string | symbol, unknown>}
*/
export function exclude_from_object(obj, keys) {
/** @type {Record<string, unknown>} */
/** @type {Record<string | symbol, unknown>} */
var result = {};
for (var key in obj) {
@ -776,6 +735,12 @@ export function exclude_from_object(obj, keys) {
}
}
for (var symbol of Object.getOwnPropertySymbols(obj)) {
if (Object.propertyIsEnumerable.call(obj, symbol) && !keys.includes(symbol)) {
result[symbol] = obj[symbol];
}
}
return result;
}

@ -1,6 +1,6 @@
import type { Store } from '#shared';
import { STATE_SYMBOL } from './constants.js';
import type { Effect, Source, Value, Reaction } from './reactivity/types.js';
import type { Effect, Source, Value } from './reactivity/types.js';
type EventCallback = (event: Event) => boolean;
export type EventCallbackMap = Record<string, EventCallback | EventCallback[]>;
@ -16,6 +16,8 @@ export type ComponentContext = {
c: null | Map<unknown, unknown>;
/** deferred effects */
e: null | Array<() => void | (() => void)>;
/** True if initialized, i.e. pop() ran */
i: boolean;
/**
* props needed for legacy mode lifecycle functions, and for `createEventDispatcher`
* @deprecated remove in 6.0

@ -64,15 +64,16 @@ export function render(component, options = {}) {
}
/**
* @param {string} hash
* @param {Renderer} renderer
* @param {(renderer: Renderer) => Promise<void> | void} fn
* @returns {void}
*/
export function head(renderer, fn) {
export function head(hash, renderer, fn) {
renderer.head((renderer) => {
renderer.push(BLOCK_OPEN);
renderer.push(`<!--${hash}-->`);
renderer.child(fn);
renderer.push(BLOCK_CLOSE);
renderer.push(EMPTY_COMMENT);
});
}

@ -100,14 +100,69 @@ export class Renderer {
}
/**
* @param {Array<Promise<void>>} blockers
* @param {(renderer: Renderer) => void} fn
*/
async(fn) {
async_block(blockers, fn) {
this.#out.push(BLOCK_OPEN);
this.child(fn);
this.async(blockers, fn);
this.#out.push(BLOCK_CLOSE);
}
/**
* @param {Array<Promise<void>>} blockers
* @param {(renderer: Renderer) => void} fn
*/
async(blockers, fn) {
let callback = fn;
if (blockers.length > 0) {
const context = ssr_context;
callback = (renderer) => {
return Promise.all(blockers).then(() => {
const previous_context = ssr_context;
try {
set_ssr_context(context);
return fn(renderer);
} finally {
set_ssr_context(previous_context);
}
});
};
}
this.child(callback);
}
/**
* @param {Array<() => void>} thunks
*/
run(thunks) {
const context = ssr_context;
let promise = Promise.resolve(thunks[0]());
const promises = [promise];
for (const fn of thunks.slice(1)) {
promise = promise.then(() => {
const previous_context = ssr_context;
set_ssr_context(context);
try {
return fn();
} finally {
set_ssr_context(previous_context);
}
});
promises.push(promise);
}
return promises;
}
/**
* Create a child renderer. The child renderer inherits the state from the parent,
* but has its own content.
@ -386,7 +441,7 @@ export class Renderer {
}
/**
* Collect all of the `onDestroy` callbacks regsitered during rendering. In an async context, this is only safe to call
* Collect all of the `onDestroy` callbacks registered during rendering. In an async context, this is only safe to call
* after awaiting `collect_async`.
*
* Child renderers are "porous" and don't affect execution order, but component body renderers

@ -137,7 +137,7 @@ const DELEGATED_EVENTS = [
* Returns `true` if `event_name` is a delegated event
* @param {string} event_name
*/
export function is_delegated(event_name) {
export function can_delegate_event(event_name) {
return DELEGATED_EVENTS.includes(event_name);
}

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

@ -201,7 +201,15 @@ export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true';
* @param {any[]} logs
*/
export function normalise_inspect_logs(logs) {
return logs.map((log) => {
/** @type {string[]} */
const normalised = [];
for (const log of logs) {
if (log === 'stack trace') {
// ignore `console.group('stack trace')` in default `$inspect(...)` output
continue;
}
if (log instanceof Error) {
const last_line = log.stack
?.trim()
@ -210,11 +218,13 @@ export function normalise_inspect_logs(logs) {
const match = last_line && /(at .+) /.exec(last_line);
return match && match[1];
if (match) normalised.push(match[1]);
} else {
normalised.push(log);
}
}
return log;
});
return normalised;
}
/**

@ -132,7 +132,11 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
flushSync();
const normalize = (string: string) =>
string.trim().replaceAll('\r\n', '\n').replaceAll('/>', '>');
string
.trim()
.replaceAll('\r\n', '\n')
.replaceAll('/>', '>')
.replace(/<!--.+?-->/g, '');
const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
assert.equal(normalize(target.innerHTML), normalize(expected));

@ -166,7 +166,7 @@ export function test(args) {
return args;
}
// TypeScript needs the type of assertions to be directly visible, not infered, which is why
// TypeScript needs the type of assertions to be directly visible, not inferred, which is why
// we can't have it on the test suite type.
/**
* @param {any} value

@ -12,6 +12,7 @@ import { raf } from '../animation-helpers.js';
import type { CompileOptions } from '#compiler';
import { suite_with_variants, type BaseTest } from '../suite.js';
import { clear } from '../../src/internal/client/reactivity/batch.js';
import { hydrating } from '../../src/internal/client/dom/hydration.js';
type Assert = typeof import('vitest').assert & {
htmlEqual(a: string, b: string, description?: string): void;
@ -533,6 +534,10 @@ async function run_test_variant(
throw err;
}
} finally {
if (hydrating) {
throw new Error('Hydration state was not cleared');
}
config.after_test?.();
// Free up the microtask queue

@ -0,0 +1,8 @@
<script>
await Promise.resolve();
let { value = $bindable("test") } = $props();
</script>
<button onclick={() => value = 'updated'}>update</button>
{value}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save