Merge branch 'main' into gh-17012

gh-17012
ComputerGuy 2 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
```
Cannot commit a fork that was already committed or discarded
Cannot commit a fork that was already discarded
```
### fork_timing

@ -1,5 +1,31 @@
# 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
### 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.42.2",
"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' */
@ -206,7 +206,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 +289,7 @@ export function analyze_module(source, options) {
});
walk(
/** @type {Node} */ (ast),
/** @type {ESTree.Node} */ (ast),
{
scope,
scopes,
@ -347,7 +347,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 +407,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);
}
@ -636,7 +636,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);

@ -1,6 +1,7 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, ExpressionMetadata, StateFields, 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;
}

@ -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, {

@ -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()');
}

@ -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

@ -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',

@ -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,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);

@ -26,7 +26,7 @@ export function RenderTag(node, context) {
let expression = build_expression(context, arg, metadata);
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));

@ -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
);

@ -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;
}
@ -295,16 +287,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;
}

@ -134,7 +134,7 @@ export function build_component(node, component_name, context) {
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))
? b.call('$.get', memoizer.add(expression, attribute.metadata.expression))
: expression
)
);
@ -149,7 +149,7 @@ export function build_component(node, component_name, context) {
build_attribute_value(attribute.value, context, (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))
? b.call('$.get', memoizer.add(value, metadata))
: value;
}).value
)
@ -185,7 +185,7 @@ export function build_component(node, component_name, context) {
});
return should_wrap_in_derived
? b.call('$.get', memoizer.add(value, metadata.has_await))
? b.call('$.get', memoizer.add(value, metadata, true))
: value;
}
);

@ -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));
}
@ -155,9 +154,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 +163,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 +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) {
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(

@ -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)
@ -23,12 +24,21 @@ export class Memoizer {
/**
* @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
(has_await ? this.#async : this.#sync).push({ id, expression });
(metadata.has_await ? this.#async : this.#sync).push({ id, expression });
return id;
}
@ -72,8 +82,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 = [];

@ -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' */
@ -90,7 +90,7 @@ const template_visitors = {
/**
* @param {ComponentAnalysis} analysis
* @param {ValidatedCompileOptions} options
* @returns {Program}
* @returns {ESTree.Program}
*/
export function server_component(analysis, options) {
/** @type {ComponentServerTransformState} */
@ -111,11 +111,11 @@ export function server_component(analysis, options) {
computed_field_declarations: null
};
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 },
@ -136,7 +136,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 },
@ -145,7 +145,7 @@ export function server_component(analysis, options) {
)
);
/** @type {VariableDeclarator[]} */
/** @type {ESTree.VariableDeclarator[]} */
const legacy_reactive_declarations = [];
for (const [node] of analysis.reactive_statements) {
@ -197,7 +197,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')),
@ -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.
// 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) {
@ -244,8 +244,8 @@ 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) {
@ -400,7 +400,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} */
@ -417,7 +417,7 @@ export function server_module(analysis, options) {
computed_field_declarations: null
};
const module = /** @type {Program} */ (
const module = /** @type {ESTree.Program} */ (
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
);

@ -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 } 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);

@ -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,5 @@
/** @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
@ -59,25 +59,38 @@ 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();
}
/**

@ -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 {
@ -1201,7 +1201,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'),

@ -109,29 +109,3 @@ export interface ComponentAnalysis extends Analysis {
*/
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'
| '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;
@ -531,7 +523,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;
};

@ -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
};
}
@ -237,8 +236,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
};
}
@ -626,8 +624,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
};
}

@ -13,6 +13,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.

@ -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);

@ -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);

@ -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';

@ -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;
/**

@ -913,28 +913,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 +953,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 +967,7 @@ export function fork(fn) {
await settled;
},
discard: () => {
if (batches.has(batch)) {
if (!committed && batches.has(batch)) {
batches.delete(batch);
batch.discard();
}

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

@ -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();

@ -609,7 +609,7 @@ export function get(signal) {
if (!tracking && !untracking && !was_read) {
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
if (trace) console.warn(trace);
}
@ -628,7 +628,7 @@ export function get(signal) {
if (signal.trace) {
signal.trace();
} else {
trace = get_stack('TracedAt');
trace = get_stack('traced at');
if (trace) {
var entry = tracing_expressions.entries.get(signal);

@ -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

@ -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.42.2';
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;
}
/**

@ -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`'
);
assert.equal(warnings[1].name, 'TracedAtError');
assert.equal(warnings[1].name, 'traced at');
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`'
);
assert.equal(warnings[1].name, 'TracedAtError');
assert.equal(warnings[1].name, 'traced at');
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 {
flushSync(() => button.click());
} 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'));
}
}

@ -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' },
'at HTMLButtonElement.on_click',
'at HTMLButtonElement.Main.button.__click',
['hello'],
'at HTMLButtonElement.on_click'
'at HTMLButtonElement.Main.button.__click'
]);
}
});

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

@ -2,12 +2,6 @@ import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
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>`);
export default function Delegated_locally_declared_shadowed($$anchor) {
@ -18,7 +12,13 @@ export default function Delegated_locally_declared_shadowed($$anchor) {
var button = root_1();
$.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);
});

@ -1,7 +1,6 @@
import 'svelte/internal/disclose-version';
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);
export default function Nullish_coallescence_omittance($$anchor) {
@ -18,7 +17,7 @@ export default function Nullish_coallescence_omittance($$anchor) {
var button = $.sibling(b, 2);
button.__click = [on_click, count];
button.__click = () => $.update(count);
var text = $.child(button);

@ -1,18 +1,19 @@
import 'svelte/internal/disclose-version';
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);
export default function State_proxy_literal($$anchor) {
let str = $.state('');
let tpl = $.state(``);
function reset() {
$.set(str, '');
$.set(str, ``);
$.set(tpl, '');
$.set(tpl, ``);
}
var fragment = root();
var input = $.first_child(fragment);
@ -24,7 +25,7 @@ export default function State_proxy_literal($$anchor) {
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_1, () => $.get(tpl), ($$value) => $.set(tpl, $$value));
$.append($$anchor, fragment);

Loading…
Cancel
Save