Merge branch 'main' into gh-17012

gh-17012
ComputerGuy 4 days ago
commit 4f757ef271

@ -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 ### fork_discarded
``` ```
Cannot commit a fork that was already committed or discarded Cannot commit a fork that was already discarded
``` ```
### fork_timing ### fork_timing

@ -1,5 +1,31 @@
# svelte # svelte
## 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 ## 5.41.4
### Patch Changes ### Patch Changes

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

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

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

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

@ -1,4 +1,4 @@
/** @import { Expression, Node, Program } from 'estree' */ /** @import * as ESTree from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */ /** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
@ -206,7 +206,7 @@ const visitors = {
* @returns {Js} * @returns {Js}
*/ */
function js(script, root, allow_reactive_declarations, parent) { function js(script, root, allow_reactive_declarations, parent) {
/** @type {Program} */ /** @type {ESTree.Program} */
const ast = script?.content ?? { const ast = script?.content ?? {
type: 'Program', type: 'Program',
sourceType: 'module', sourceType: 'module',
@ -289,7 +289,7 @@ export function analyze_module(source, options) {
}); });
walk( walk(
/** @type {Node} */ (ast), /** @type {ESTree.Node} */ (ast),
{ {
scope, scope,
scopes, scopes,
@ -347,7 +347,7 @@ export function analyze_component(root, source, options) {
const store_name = name.slice(1); const store_name = name.slice(1);
const declaration = instance.scope.get(store_name); 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 // If we're not in legacy mode through the compiler option, assume the user
// is referencing a rune and not a global store. // is referencing a rune and not a global store.
@ -407,7 +407,7 @@ export function analyze_component(root, source, options) {
/** @type {number} */ (node.start) > /** @type {number} */ (module.ast.start) && /** @type {number} */ (node.start) > /** @type {number} */ (module.ast.start) &&
/** @type {number} */ (node.end) < /** @type {number} */ (module.ast.end) && /** @type {number} */ (node.end) < /** @type {number} */ (module.ast.end) &&
// const state = $state(0) is valid // 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); e.store_invalid_subscription(node);
} }
@ -636,7 +636,7 @@ export function analyze_component(root, source, options) {
// @ts-expect-error // @ts-expect-error
_: set_scope, _: set_scope,
Identifier(node, context) { 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)) { if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name); const binding = context.state.scope.get(node.name);

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

@ -1,12 +1,7 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */ /** @import { AST } from '#compiler' */
/** @import { AST, DelegatedEvent } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js'; import { cannot_be_set_statically, can_delegate_event } from '../../../../utils.js';
import { import { get_attribute_chunks, is_event_attribute } from '../../../utils/ast.js';
get_attribute_chunks,
get_attribute_expression,
is_event_attribute
} from '../../../utils/ast.js';
import { mark_subtree_dynamic } from './shared/fragment.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; context.state.analysis.uses_event_attributes = true;
} }
const expression = get_attribute_expression(node); node.metadata.delegated =
const delegated_event = get_delegated_event(node.name.slice(2), expression, context); parent?.type === 'RegularElement' && can_delegate_event(node.name.slice(2));
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;
}
}
} }
// 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;
} }

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

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

@ -6,13 +6,6 @@
* @param {Context} context * @param {Context} context
*/ */
export function visit_function(node, 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) { if (context.state.expression) {
for (const [name] of context.state.scope.references) { for (const [name] of context.state.scope.references) {
const binding = context.state.scope.get(name); 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); const binding = context.state.scope.get(argument.name);
if (context.state.analysis.runes) { 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()'); e.constant_assignment(node, '$props.id()');
} }

@ -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 { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { ClientTransformState, ComponentClientTransformState } from './types.js' */
/** @import { Analysis } from '../../types.js' */ /** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */ /** @import { Scope } from '../../scope.js' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
@ -12,9 +12,6 @@ import {
PROPS_IS_UPDATED, PROPS_IS_UPDATED,
PROPS_IS_BINDABLE PROPS_IS_BINDABLE
} from '../../../../constants.js'; } from '../../../../constants.js';
import { dev } from '../../../state.js';
import { walk } from 'zimmerframe';
import { validate_mutation } from './visitors/shared/utils.js';
/** /**
* @param {Binding} binding * @param {Binding} binding
@ -46,125 +43,6 @@ export function build_getter(node, state) {
return node; 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 {Binding} binding
* @param {ComponentClientTransformState} state * @param {ComponentClientTransformState} state

@ -62,6 +62,17 @@ export function CallExpression(node, context) {
is_ignored(node, 'state_snapshot_uncloneable') && b.true 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': case '$effect.root':
return b.call( return b.call(
'$.effect_root', '$.effect_root',

@ -11,16 +11,6 @@ export function ExpressionStatement(node, context) {
if (node.expression.type === 'CallExpression') { if (node.expression.type === 'CallExpression') {
const rune = get_rune(node.expression, context.state.scope); 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') { if (rune === '$inspect.trace') {
return b.empty; return b.empty;
} }

@ -1,7 +1,5 @@
/** @import { FunctionDeclaration } from 'estree' */ /** @import { FunctionDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { build_hoisted_params } from '../utils.js';
import * as b from '#compiler/builders';
/** /**
* @param {FunctionDeclaration} node * @param {FunctionDeclaration} node
@ -10,14 +8,5 @@ import * as b from '#compiler/builders';
export function FunctionDeclaration(node, context) { export function FunctionDeclaration(node, context) {
const state = { ...context.state, in_constructor: false, in_derived: false }; 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); context.next(state);
} }

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

@ -26,7 +26,7 @@ export function RenderTag(node, context) {
let expression = build_expression(context, arg, metadata); let expression = build_expression(context, arg, metadata);
if (metadata.has_await || metadata.has_call) { if (metadata.has_await || metadata.has_call) {
expression = b.call('$.get', memoizer.add(expression, metadata.has_await)); expression = b.call('$.get', memoizer.add(expression, metadata));
} }
args.push(b.thunk(expression)); args.push(b.thunk(expression));

@ -35,7 +35,7 @@ export function SlotElement(node, context) {
context, context,
(value, metadata) => (value, metadata) =>
metadata.has_call || metadata.has_await metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata.has_await)) ? b.call('$.get', memoizer.add(value, metadata))
: value : value
); );

@ -7,7 +7,6 @@ import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js'; import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.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'; import { get_value } from './shared/declarations.js';
/** /**
@ -32,13 +31,6 @@ export function VariableDeclaration(node, context) {
rune === '$state.snapshot' || rune === '$state.snapshot' ||
rune === '$host' 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))); declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue; continue;
} }
@ -295,16 +287,6 @@ export function VariableDeclaration(node, context) {
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop'); const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (!has_state && !has_props) { 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))); declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue; continue;
} }

@ -134,7 +134,7 @@ export function build_component(node, component_name, context) {
props_and_spreads.push( props_and_spreads.push(
b.thunk( b.thunk(
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call attribute.metadata.expression.has_await || attribute.metadata.expression.has_call
? b.call('$.get', memoizer.add(expression, attribute.metadata.expression.has_await)) ? b.call('$.get', memoizer.add(expression, attribute.metadata.expression))
: expression : expression
) )
); );
@ -149,7 +149,7 @@ export function build_component(node, component_name, context) {
build_attribute_value(attribute.value, context, (value, metadata) => { build_attribute_value(attribute.value, context, (value, metadata) => {
// TODO put the derived in the local block // TODO put the derived in the local block
return metadata.has_call || metadata.has_await return metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata.has_await)) ? b.call('$.get', memoizer.add(value, metadata))
: value; : value;
}).value }).value
) )
@ -185,7 +185,7 @@ export function build_component(node, component_name, context) {
}); });
return should_wrap_in_derived return should_wrap_in_derived
? b.call('$.get', memoizer.add(value, metadata.has_await)) ? b.call('$.get', memoizer.add(value, metadata, true))
: value; : value;
} }
); );

@ -1,5 +1,5 @@
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */ /** @import { Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types' */ /** @import { ComponentContext } from '../../types' */
import { escape_html } from '../../../../../../escaping.js'; import { escape_html } from '../../../../../../escaping.js';
import { normalize_attribute } from '../../../../../../utils.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 * as b from '#compiler/builders';
import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js'; import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js';
import { build_expression, build_template_chunk, Memoizer } from './utils.js'; import { build_expression, build_template_chunk, Memoizer } from './utils.js';
import { ExpressionMetadata } from '../../../../nodes.js';
/** /**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes * @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
@ -35,7 +36,7 @@ export function build_attribute_effect(
for (const attribute of attributes) { for (const attribute of attributes) {
if (attribute.type === 'Attribute') { if (attribute.type === 'Attribute') {
const { value } = build_attribute_value(attribute.value, context, (value, metadata) => 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 ( if (
@ -52,9 +53,7 @@ export function build_attribute_effect(
} else { } else {
let value = /** @type {Expression} */ (context.visit(attribute)); 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);
value = memoizer.add(value, attribute.metadata.expression.has_await);
}
values.push(b.spread(value)); values.push(b.spread(value));
} }
@ -155,9 +154,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
value = b.call('$.clsx', value); value = b.call('$.clsx', value);
} }
return metadata.has_call || metadata.has_await return context.state.memoizer.add(value, metadata);
? context.state.memoizer.add(value, metadata.has_await)
: value;
}); });
/** @type {Identifier | undefined} */ /** @type {Identifier | undefined} */
@ -166,7 +163,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
/** @type {ObjectExpression | Identifier | undefined} */ /** @type {ObjectExpression | Identifier | undefined} */
let prev; let prev;
/** @type {ObjectExpression | Identifier | undefined} */ /** @type {Expression | undefined} */
let next; let next;
if (class_directives.length) { if (class_directives.length) {
@ -227,7 +224,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
*/ */
export function build_set_style(node_id, attribute, style_directives, context) { export function build_set_style(node_id, attribute, style_directives, context) {
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => 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} */ /** @type {Identifier | undefined} */

@ -1,9 +1,10 @@
/** @import { Expression } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types' */ /** @import { ComponentContext } from '../../types' */
import { is_capture_event, is_passive_event } from '../../../../../../utils.js'; import { is_capture_event, is_passive_event } from '../../../../../../utils.js';
import { dev, locator } from '../../../../../state.js'; import { dev, locator } from '../../../../../state.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { ExpressionMetadata } from '../../../../nodes.js';
/** /**
* @param {AST.Attribute} node * @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); let handler = build_event_handler(tag.expression, tag.metadata.expression, context);
if (node.metadata.delegated) { if (node.metadata.delegated) {
let delegated_assignment;
if (!context.state.events.has(event_name)) { if (!context.state.events.has(event_name)) {
context.state.events.add(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( context.state.init.push(
b.stmt( b.stmt(b.assignment('=', b.member(context.state.node, '__' + event_name), handler))
b.assignment('=', b.member(context.state.node, '__' + event_name), delegated_assignment)
)
); );
} else { } else {
const statement = b.stmt( const statement = b.stmt(

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

@ -1,5 +1,5 @@
/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */ /** @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 { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js'; 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 is_reference from 'is-reference';
import { dev, is_ignored, locator, component_name } from '../../../../../state.js'; import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
import { build_getter } from '../../utils.js'; import { build_getter } from '../../utils.js';
import { ExpressionMetadata } from '../../../../nodes.js';
/** /**
* A utility for extracting complex expressions (such as call expressions) * A utility for extracting complex expressions (such as call expressions)
@ -23,12 +24,21 @@ export class Memoizer {
/** /**
* @param {Expression} expression * @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) {
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 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; return id;
} }
@ -72,8 +82,7 @@ export function build_template_chunk(
values, values,
context, context,
state = context.state, state = context.state,
memoize = (value, metadata) => memoize = (value, metadata) => state.memoizer.add(value, metadata)
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value
) { ) {
/** @type {Expression[]} */ /** @type {Expression[]} */
const expressions = []; const expressions = [];

@ -1,4 +1,4 @@
/** @import { Program, Property, Statement, VariableDeclarator } from 'estree' */ /** @import * as ESTree from 'estree' */
/** @import { AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentServerTransformState, ComponentVisitors, ServerTransformState, Visitors } from './types.js' */ /** @import { ComponentServerTransformState, ComponentVisitors, ServerTransformState, Visitors } from './types.js' */
/** @import { Analysis, ComponentAnalysis } from '../../types.js' */ /** @import { Analysis, ComponentAnalysis } from '../../types.js' */
@ -90,7 +90,7 @@ const template_visitors = {
/** /**
* @param {ComponentAnalysis} analysis * @param {ComponentAnalysis} analysis
* @param {ValidatedCompileOptions} options * @param {ValidatedCompileOptions} options
* @returns {Program} * @returns {ESTree.Program}
*/ */
export function server_component(analysis, options) { export function server_component(analysis, options) {
/** @type {ComponentServerTransformState} */ /** @type {ComponentServerTransformState} */
@ -111,11 +111,11 @@ export function server_component(analysis, options) {
computed_field_declarations: null computed_field_declarations: null
}; };
const module = /** @type {Program} */ ( const module = /** @type {ESTree.Program} */ (
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors) walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
); );
const instance = /** @type {Program} */ ( const instance = /** @type {ESTree.Program} */ (
walk( walk(
/** @type {AST.SvelteNode} */ (analysis.instance.ast), /** @type {AST.SvelteNode} */ (analysis.instance.ast),
{ ...state, scopes: analysis.instance.scopes }, { ...state, scopes: analysis.instance.scopes },
@ -136,7 +136,7 @@ export function server_component(analysis, options) {
) )
); );
const template = /** @type {Program} */ ( const template = /** @type {ESTree.Program} */ (
walk( walk(
/** @type {AST.SvelteNode} */ (analysis.template.ast), /** @type {AST.SvelteNode} */ (analysis.template.ast),
{ ...state, scopes: analysis.template.scopes }, { ...state, scopes: analysis.template.scopes },
@ -145,7 +145,7 @@ export function server_component(analysis, options) {
) )
); );
/** @type {VariableDeclarator[]} */ /** @type {ESTree.VariableDeclarator[]} */
const legacy_reactive_declarations = []; const legacy_reactive_declarations = [];
for (const [node] of analysis.reactive_statements) { for (const [node] of analysis.reactive_statements) {
@ -197,7 +197,7 @@ export function server_component(analysis, options) {
b.function_declaration( b.function_declaration(
b.id('$$render_inner'), b.id('$$render_inner'),
[b.id('$$renderer')], [b.id('$$renderer')],
b.block(/** @type {Statement[]} */ (rest)) b.block(/** @type {ESTree.Statement[]} */ (rest))
), ),
b.do_while( b.do_while(
b.unary('!', b.id('$$settled')), b.unary('!', b.id('$$settled')),
@ -224,7 +224,7 @@ export function server_component(analysis, options) {
// Propagate values of bound props upwards if they're undefined in the parent and have a value. // 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. // 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 = []; const props = [];
for (const [name, binding] of analysis.instance.scope.declarations) { for (const [name, binding] of analysis.instance.scope.declarations) {
@ -244,8 +244,8 @@ export function server_component(analysis, options) {
} }
let component_block = b.block([ let component_block = b.block([
.../** @type {Statement[]} */ (instance.body), .../** @type {ESTree.Statement[]} */ (instance.body),
.../** @type {Statement[]} */ (template.body) .../** @type {ESTree.Statement[]} */ (template.body)
]); ]);
if (analysis.instance.has_await) { if (analysis.instance.has_await) {
@ -400,7 +400,7 @@ export function server_component(analysis, options) {
/** /**
* @param {Analysis} analysis * @param {Analysis} analysis
* @param {ValidatedModuleCompileOptions} options * @param {ValidatedModuleCompileOptions} options
* @returns {Program} * @returns {ESTree.Program}
*/ */
export function server_module(analysis, options) { export function server_module(analysis, options) {
/** @type {ServerTransformState} */ /** @type {ServerTransformState} */
@ -417,7 +417,7 @@ export function server_module(analysis, options) {
computed_field_declarations: null computed_field_declarations: null
}; };
const module = /** @type {Program} */ ( const module = /** @type {ESTree.Program} */ (
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors) walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
); );

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

@ -1,5 +1,5 @@
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */ /** @import { Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */ /** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { escape_html } from '../../../../../../escaping.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 { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.js'; import { regex_whitespaces_strict } from '../../../../patterns.js';
import { has_await_expression } from '../../../../../utils/ast.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 */ /** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
export const block_open = b.literal(BLOCK_OPEN); export const block_open = b.literal(BLOCK_OPEN);

@ -1,36 +1,17 @@
/** @import { Context } from 'zimmerframe' */
/** @import { TransformState } from './types.js' */ /** @import { TransformState } from './types.js' */
/** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */ /** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */
/** @import { Node, Expression, CallExpression, MemberExpression } from 'estree' */ /** @import { Node, Expression, CallExpression, MemberExpression } from 'estree' */
import { import {
regex_ends_with_whitespaces, regex_ends_with_whitespaces,
regex_not_whitespace, regex_not_whitespace,
regex_starts_with_newline,
regex_starts_with_whitespaces regex_starts_with_whitespaces
} from '../patterns.js'; } from '../patterns.js';
import * as b from '#compiler/builders';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { extract_identifiers } from '../../utils/ast.js'; import { extract_identifiers } from '../../utils/ast.js';
import check_graph_for_cycles from '../2-analyze/utils/check_graph_for_cycles.js'; import check_graph_for_cycles from '../2-analyze/utils/check_graph_for_cycles.js';
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { set_scope } from '../scope.js'; 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 * Match Svelte 4 behaviour by sorting ConstTag nodes in topological order

@ -1,5 +1,5 @@
/** @import { Expression, PrivateIdentifier } from 'estree' */ /** @import { Expression, PrivateIdentifier } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { AST, Binding } from '#compiler' */
/** /**
* All nodes that can appear elsewhere than the top level, have attributes and can contain children * All nodes that can appear elsewhere than the top level, have attributes and can contain children
@ -59,25 +59,38 @@ export function create_attribute(name, start, end, value) {
name, name,
value, value,
metadata: { metadata: {
delegated: null, delegated: false,
needs_clsx: false needs_clsx: false
} }
}; };
} }
export class ExpressionMetadata {
/** True if the expression references state directly, or _might_ (via member/call expressions) */
has_state = false;
/** /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
* @returns {ExpressionMetadata} has_call = false;
*/
export function create_expression_metadata() { /** True if the expression contains `await` */
return { has_await = false;
dependencies: new Set(),
references: new Set(), /** True if the expression includes a member expression */
has_state: false, has_member_expression = false;
has_call: false,
has_member_expression: false, /** True if the expression includes an assignment or an update */
has_assignment: false, has_assignment = false;
has_await: 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();
} }
/** /**

@ -3,7 +3,7 @@
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { create_expression_metadata } from './nodes.js'; import { ExpressionMetadata } from './nodes.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import * as e from '../errors.js'; import * as e from '../errors.js';
import { import {
@ -1201,7 +1201,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (node.fallback) visit(node.fallback, { scope }); if (node.fallback) visit(node.fallback, { scope });
node.metadata = { node.metadata = {
expression: create_expression_metadata(), expression: new ExpressionMetadata(),
keyed: false, keyed: false,
contains_group_binding: false, contains_group_binding: false,
index: scope.root.unique('$$index'), index: scope.root.unique('$$index'),

@ -109,29 +109,3 @@ export interface ComponentAnalysis extends Analysis {
*/ */
snippets: Set<AST.SnippetBlock>; 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;
};
}
}

@ -295,23 +295,6 @@ export type DeclarationKind =
| 'using' | 'using'
| 'await 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 { export interface StateField {
type: StateCreationRuneName; type: StateCreationRuneName;
node: PropertyDefinition | AssignmentExpression; node: PropertyDefinition | AssignmentExpression;

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

@ -42,8 +42,7 @@ export function arrow(params, body, async = false) {
body, body,
expression: body.type !== 'BlockStatement', expression: body.type !== 'BlockStatement',
generator: false, generator: false,
async, async
metadata: /** @type {any} */ (null) // should not be used by codegen
}; };
} }
@ -237,8 +236,7 @@ export function function_declaration(id, params, body, async = false) {
params, params,
body, body,
generator: false, generator: false,
async, async
metadata: /** @type {any} */ (null) // should not be used by codegen
}; };
} }
@ -626,8 +624,7 @@ function function_builder(id, params, body, async = false) {
params, params,
body, body,
generator: false, generator: false,
async, async
metadata: /** @type {any} */ (null) // should not be used by codegen
}; };
} }

@ -13,6 +13,7 @@ export const INERT = 1 << 13;
export const DESTROYED = 1 << 14; export const DESTROYED = 1 << 14;
// Flags exclusive to effects // Flags exclusive to effects
/** Set once an effect that should run synchronously has run */
export const EFFECT_RAN = 1 << 15; export const EFFECT_RAN = 1 << 15;
/** /**
* 'Transparent' effects do not create a transition boundary. * 'Transparent' effects do not create a transition boundary.

@ -128,7 +128,11 @@ export function setContext(key, context) {
if (async_mode_flag) { if (async_mode_flag) {
var flags = /** @type {Effect} */ (active_effect).f; 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) { if (!valid) {
e.set_context_after_init(); e.set_context_after_init();
@ -173,6 +177,7 @@ export function getAllContexts() {
export function push(props, runes = false, fn) { export function push(props, runes = false, fn) {
component_context = { component_context = {
p: component_context, p: component_context,
i: false,
c: null, c: null,
e: null, e: null,
s: props, s: props,
@ -208,6 +213,8 @@ export function pop(component) {
context.x = component; context.x = component;
} }
context.i = true;
component_context = context.p; component_context = context.p;
if (DEV) { if (DEV) {

@ -33,8 +33,17 @@ export function inspect(get_value, inspector, show_stack = false) {
inspector(...snap); inspector(...snap);
if (!initial) { if (!initial) {
const stack = get_stack('$inspect(...)');
// eslint-disable-next-line no-console // 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 { } else {
inspector(initial ? 'init' : 'update', ...snap); inspector(initial ? 'init' : 'update', ...snap);

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

@ -7,7 +7,7 @@ import { add_form_reset_listener, autofocus } from './misc.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { LOADING_ATTR_SYMBOL } from '#client/constants'; import { LOADING_ATTR_SYMBOL } from '#client/constants';
import { queue_micro_task } from '../task.js'; 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 { import {
active_effect, active_effect,
active_reaction, active_reaction,
@ -378,7 +378,7 @@ function set_attributes(
const opts = {}; const opts = {};
const event_handle_key = '$$' + key; const event_handle_key = '$$' + key;
let event_name = key.slice(2); 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)) { if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7); event_name = event_name.slice(0, -7);

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

@ -29,7 +29,7 @@ export function handle_error(error) {
// if the error occurred while creating this subtree, we let it // if the error occurred while creating this subtree, we let it
// bubble up until it hits a boundary that can handle it // bubble up until it hits a boundary that can handle it
if ((effect.f & BOUNDARY_EFFECT) === 0) { if ((effect.f & BOUNDARY_EFFECT) === 0) {
if (!effect.parent && error instanceof Error) { if (DEV && !effect.parent && error instanceof Error) {
apply_adjustments(error); apply_adjustments(error);
} }
@ -61,7 +61,7 @@ export function invoke_error_boundary(error, effect) {
effect = effect.parent; effect = effect.parent;
} }
if (error instanceof Error) { if (DEV && error instanceof Error) {
apply_adjustments(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} * @returns {never}
*/ */
export function fork_discarded() { export function fork_discarded() {
if (DEV) { 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'; error.name = 'Svelte error';

@ -53,7 +53,7 @@ export function proxy(value) {
var is_proxied_array = is_array(value); var is_proxied_array = is_array(value);
var version = source(0); 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; var parent_version = update_version;
/** /**

@ -913,28 +913,36 @@ export function fork(fn) {
e.fork_timing(); e.fork_timing();
} }
const batch = Batch.ensure(); var batch = Batch.ensure();
batch.is_fork = true; batch.is_fork = true;
const settled = batch.settled(); var committed = false;
var settled = batch.settled();
flushSync(fn); flushSync(fn);
// revert state changes // revert state changes
for (const [source, value] of batch.previous) { for (var [source, value] of batch.previous) {
source.v = value; source.v = value;
} }
return { return {
commit: async () => { commit: async () => {
if (committed) {
await settled;
return;
}
if (!batches.has(batch)) { if (!batches.has(batch)) {
e.fork_discarded(); e.fork_discarded();
} }
committed = true;
batch.is_fork = false; batch.is_fork = false;
// apply changes // apply changes
for (const [source, value] of batch.current) { for (var [source, value] of batch.current) {
source.v = value; source.v = value;
} }
@ -945,9 +953,9 @@ export function fork(fn) {
// TODO maybe there's a better implementation? // TODO maybe there's a better implementation?
flushSync(() => { flushSync(() => {
/** @type {Set<Effect>} */ /** @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); mark_eager_effects(source, eager_effects);
} }
@ -959,7 +967,7 @@ export function fork(fn) {
await settled; await settled;
}, },
discard: () => { discard: () => {
if (batches.has(batch)) { if (!committed && batches.has(batch)) {
batches.delete(batch); batches.delete(batch);
batch.discard(); batch.discard();
} }

@ -86,7 +86,7 @@ export function derived(fn) {
}; };
if (DEV && tracing_mode_flag) { if (DEV && tracing_mode_flag) {
signal.created = get_stack('CreatedAt'); signal.created = get_stack('created at');
} }
return signal; return signal;

@ -76,7 +76,7 @@ export function source(v, stack) {
}; };
if (DEV && tracing_mode_flag) { if (DEV && tracing_mode_flag) {
signal.created = stack ?? get_stack('CreatedAt'); signal.created = stack ?? get_stack('created at');
signal.updated = null; signal.updated = null;
signal.set_during_effect = false; signal.set_during_effect = false;
signal.trace = null; signal.trace = null;
@ -186,7 +186,7 @@ export function internal_set(source, value) {
if (DEV) { if (DEV) {
if (tracing_mode_flag || active_effect !== null) { if (tracing_mode_flag || active_effect !== null) {
const error = get_stack('UpdatedAt'); const error = get_stack('updated at');
if (error !== null) { if (error !== null) {
source.updated ??= new Map(); source.updated ??= new Map();

@ -609,7 +609,7 @@ export function get(signal) {
if (!tracking && !untracking && !was_read) { if (!tracking && !untracking && !was_read) {
w.await_reactivity_loss(/** @type {string} */ (signal.label)); w.await_reactivity_loss(/** @type {string} */ (signal.label));
var trace = get_stack('TracedAt'); var trace = get_stack('traced at');
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
if (trace) console.warn(trace); if (trace) console.warn(trace);
} }
@ -628,7 +628,7 @@ export function get(signal) {
if (signal.trace) { if (signal.trace) {
signal.trace(); signal.trace();
} else { } else {
trace = get_stack('TracedAt'); trace = get_stack('traced at');
if (trace) { if (trace) {
var entry = tracing_expressions.entries.get(signal); var entry = tracing_expressions.entries.get(signal);

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

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

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

@ -201,7 +201,15 @@ export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true';
* @param {any[]} logs * @param {any[]} logs
*/ */
export function normalise_inspect_logs(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) { if (log instanceof Error) {
const last_line = log.stack const last_line = log.stack
?.trim() ?.trim()
@ -210,11 +218,13 @@ export function normalise_inspect_logs(logs) {
const match = last_line && /(at .+) /.exec(last_line); 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;
});
} }
/** /**

@ -0,0 +1,11 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test() {
// else runtime_error is checked too soon
await tick();
},
runtime_error: 'set_context_after_init'
});

@ -0,0 +1,7 @@
<script>
import { setContext } from 'svelte';
await Promise.resolve('hi');
setContext('key', 'value');
</script>

@ -17,7 +17,7 @@ export default test({
'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`' 'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`'
); );
assert.equal(warnings[1].name, 'TracedAtError'); assert.equal(warnings[1].name, 'traced at');
assert.equal(warnings.length, 2); assert.equal(warnings.length, 2);
} }

@ -20,7 +20,7 @@ export default test({
'Detected reactivity loss when reading `b`. This happens when state is read in an async function after an earlier `await`' 'Detected reactivity loss when reading `b`. This happens when state is read in an async function after an earlier `await`'
); );
assert.equal(warnings[1].name, 'TracedAtError'); assert.equal(warnings[1].name, 'traced at');
assert.equal(warnings.length, 2); assert.equal(warnings.length, 2);
} }

@ -0,0 +1,7 @@
<script lang="ts">
import { getContext } from "svelte";
let greeting = getContext("greeting");
</script>
<p>{greeting}</p>

@ -0,0 +1,9 @@
<script lang="ts">
import { setContext } from "svelte";
import Inner from "./Inner.svelte";
setContext("greeting", "hi");
await Promise.resolve();
</script>
<Inner />

@ -0,0 +1,11 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'async-server'],
ssrHtml: `<p>hi</p>`,
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<p>hi</p>');
}
});

@ -0,0 +1,7 @@
<script lang="ts">
import Outer from "./Outer.svelte";
await Promise.resolve();
</script>
<Outer />

@ -14,7 +14,7 @@ export default test({
try { try {
flushSync(() => button.click()); flushSync(() => button.click());
} catch (e) { } catch (e) {
assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be UpdatedAtError assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be 'updated at'
assert.ok(/** @type {Error} */ (e).message.startsWith('effect_update_depth_exceeded')); assert.ok(/** @type {Error} */ (e).message.startsWith('effect_update_depth_exceeded'));
} }
} }

@ -0,0 +1,6 @@
import { test } from '../../test';
export default test({
error: 'x is not defined',
async test() {}
});

@ -15,9 +15,9 @@ export default test({
{}, {},
[], [],
{ x: 'hello' }, { x: 'hello' },
'at HTMLButtonElement.on_click', 'at HTMLButtonElement.Main.button.__click',
['hello'], ['hello'],
'at HTMLButtonElement.on_click' 'at HTMLButtonElement.Main.button.__click'
]); ]);
} }
}); });

@ -15,9 +15,9 @@ export default test({
assert.deepEqual(normalise_inspect_logs(logs), [ assert.deepEqual(normalise_inspect_logs(logs), [
[], [],
[{}], [{}],
'at HTMLButtonElement.on_click', 'at HTMLButtonElement.Main.button.__click',
[{}, {}], [{}, {}],
'at HTMLButtonElement.on_click' 'at HTMLButtonElement.Main.button.__click'
]); ]);
} }
}); });

@ -1,19 +1,20 @@
import 'svelte/internal/disclose-version'; import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client'; import * as $ from 'svelte/internal/client';
function increment(_, counter) {
counter.count += 1;
}
var root = $.from_html(`<button> </button> <!> `, 1); var root = $.from_html(`<button> </button> <!> `, 1);
export default function Await_block_scope($$anchor) { export default function Await_block_scope($$anchor) {
let counter = $.proxy({ count: 0 }); let counter = $.proxy({ count: 0 });
const promise = $.derived(() => Promise.resolve(counter)); const promise = $.derived(() => Promise.resolve(counter));
function increment() {
counter.count += 1;
}
var fragment = root(); var fragment = root();
var button = $.first_child(fragment); var button = $.first_child(fragment);
button.__click = [increment, counter]; button.__click = increment;
var text = $.child(button); var text = $.child(button);

@ -2,12 +2,6 @@ import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy'; import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client'; import * as $ from 'svelte/internal/client';
var on_click = (e) => {
const index = Number(e.currentTarget.dataset.index);
console.log(index);
};
var root_1 = $.from_html(`<button type="button">B</button>`); var root_1 = $.from_html(`<button type="button">B</button>`);
export default function Delegated_locally_declared_shadowed($$anchor) { export default function Delegated_locally_declared_shadowed($$anchor) {
@ -18,7 +12,13 @@ export default function Delegated_locally_declared_shadowed($$anchor) {
var button = root_1(); var button = root_1();
$.set_attribute(button, 'data-index', index); $.set_attribute(button, 'data-index', index);
button.__click = [on_click];
button.__click = (e) => {
const index = Number(e.currentTarget.dataset.index);
console.log(index);
};
$.append($$anchor, button); $.append($$anchor, button);
}); });

@ -1,7 +1,6 @@
import 'svelte/internal/disclose-version'; import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client'; import * as $ from 'svelte/internal/client';
var on_click = (_, count) => $.update(count);
var root = $.from_html(`<h1></h1> <b></b> <button> </button> <h1></h1>`, 1); var root = $.from_html(`<h1></h1> <b></b> <button> </button> <h1></h1>`, 1);
export default function Nullish_coallescence_omittance($$anchor) { export default function Nullish_coallescence_omittance($$anchor) {
@ -18,7 +17,7 @@ export default function Nullish_coallescence_omittance($$anchor) {
var button = $.sibling(b, 2); var button = $.sibling(b, 2);
button.__click = [on_click, count]; button.__click = () => $.update(count);
var text = $.child(button); var text = $.child(button);

@ -1,18 +1,19 @@
import 'svelte/internal/disclose-version'; import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client'; import * as $ from 'svelte/internal/client';
function reset(_, str, tpl) {
$.set(str, '');
$.set(str, ``);
$.set(tpl, '');
$.set(tpl, ``);
}
var root = $.from_html(`<input/> <input/> <button>reset</button>`, 1); var root = $.from_html(`<input/> <input/> <button>reset</button>`, 1);
export default function State_proxy_literal($$anchor) { export default function State_proxy_literal($$anchor) {
let str = $.state(''); let str = $.state('');
let tpl = $.state(``); let tpl = $.state(``);
function reset() {
$.set(str, '');
$.set(str, ``);
$.set(tpl, '');
$.set(tpl, ``);
}
var fragment = root(); var fragment = root();
var input = $.first_child(fragment); var input = $.first_child(fragment);
@ -24,7 +25,7 @@ export default function State_proxy_literal($$anchor) {
var button = $.sibling(input_1, 2); var button = $.sibling(input_1, 2);
button.__click = [reset, str, tpl]; button.__click = reset;
$.bind_value(input, () => $.get(str), ($$value) => $.set(str, $$value)); $.bind_value(input, () => $.get(str), ($$value) => $.set(str, $$value));
$.bind_value(input_1, () => $.get(tpl), ($$value) => $.set(tpl, $$value)); $.bind_value(input_1, () => $.get(tpl), ($$value) => $.set(tpl, $$value));
$.append($$anchor, fragment); $.append($$anchor, fragment);

Loading…
Cancel
Save