Merge branch 'main' into boundary-batch-nullpointer-fix

boundary-batch-nullpointer-fix
Simon Holthausen 2 weeks ago
commit 626b9fcf2b

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: place instance-level snippets inside async body

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: restore batch along with effect context

@ -196,6 +196,51 @@ Cyclical dependency detected: %cycle%
`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary` or `<Component>`
```
### const_tag_invalid_reference
```
The `{@const %name% = ...}` declaration is not available in this snippet
```
The following is an error:
```svelte
<svelte:boundary>
{@const foo = 'bar'}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
```
Here, `foo` is not available inside `failed`. The top level code inside `<svelte:boundary>` becomes part of the implicit `children` snippet, in other words the above code is equivalent to this:
```svelte
<svelte:boundary>
{#snippet children()}
{@const foo = 'bar'}
{/snippet}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
```
The same applies to components:
```svelte
<Component>
{@const foo = 'bar'}
{#snippet someProp()}
<!-- error -->
{foo}
{/snippet}
</Component>
```
### constant_assignment
```

@ -1,5 +1,29 @@
# svelte
## 5.38.3
### Patch Changes
- fix: ensure correct order of template effect values ([#16655](https://github.com/sveltejs/svelte/pull/16655))
- fix: allow async `{@const}` in more places ([#16643](https://github.com/sveltejs/svelte/pull/16643))
- fix: properly catch top level await errors ([#16619](https://github.com/sveltejs/svelte/pull/16619))
- perf: prune effects without dependencies ([#16625](https://github.com/sveltejs/svelte/pull/16625))
- fix: only emit `for_await_track_reactivity_loss` in async mode ([#16644](https://github.com/sveltejs/svelte/pull/16644))
## 5.38.2
### Patch Changes
- perf: run blocks eagerly during flush instead of aborting ([#16631](https://github.com/sveltejs/svelte/pull/16631))
- fix: don't clone non-proxies in `$inspect` ([#16617](https://github.com/sveltejs/svelte/pull/16617))
- fix: avoid recursion error when tagging circular references ([#16622](https://github.com/sveltejs/svelte/pull/16622))
## 5.38.1
### Patch Changes

@ -124,6 +124,49 @@
> `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary` or `<Component>`
## const_tag_invalid_reference
> The `{@const %name% = ...}` declaration is not available in this snippet
The following is an error:
```svelte
<svelte:boundary>
{@const foo = 'bar'}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
```
Here, `foo` is not available inside `failed`. The top level code inside `<svelte:boundary>` becomes part of the implicit `children` snippet, in other words the above code is equivalent to this:
```svelte
<svelte:boundary>
{#snippet children()}
{@const foo = 'bar'}
{/snippet}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
```
The same applies to components:
```svelte
<Component>
{@const foo = 'bar'}
{#snippet someProp()}
<!-- error -->
{foo}
{/snippet}
</Component>
```
## debug_tag_invalid_arguments
> {@debug ...} arguments must be identifiers, not arbitrary expressions

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

@ -985,6 +985,16 @@ export function const_tag_invalid_placement(node) {
e(node, 'const_tag_invalid_placement', `\`{@const}\` must be the immediate child of \`{#snippet}\`, \`{#if}\`, \`{:else if}\`, \`{:else}\`, \`{#each}\`, \`{:then}\`, \`{:catch}\`, \`<svelte:fragment>\`, \`<svelte:boundary\` or \`<Component>\`\nhttps://svelte.dev/e/const_tag_invalid_placement`);
}
/**
* The `{@const %name% = ...}` declaration is not available in this snippet
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function const_tag_invalid_reference(node, name) {
e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet \nhttps://svelte.dev/e/const_tag_invalid_reference`);
}
/**
* {@debug ...} arguments must be identifiers, not arbitrary expressions
* @param {null | number | NodeLike} node

@ -7,6 +7,7 @@ import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { get_rune } from '../../scope.js';
import { is_component_node } from '../../nodes.js';
/**
* @param {Identifier} node
@ -155,5 +156,37 @@ export function Identifier(node, context) {
) {
w.reactive_declaration_module_script_dependency(node);
}
if (binding.metadata?.is_template_declaration && context.state.options.experimental.async) {
let snippet_name;
// Find out if this references a {@const ...} declaration of an implicit children snippet
// when it is itself inside a snippet block at the same level. If so, error.
for (let i = context.path.length - 1; i >= 0; i--) {
const parent = context.path[i];
const grand_parent = context.path[i - 1];
if (parent.type === 'SnippetBlock') {
snippet_name = parent.expression.name;
} else if (
snippet_name &&
grand_parent &&
parent.type === 'Fragment' &&
(is_component_node(grand_parent) ||
(grand_parent.type === 'SvelteBoundary' &&
(snippet_name === 'failed' || snippet_name === 'pending')))
) {
if (
is_component_node(grand_parent)
? grand_parent.metadata.scopes.default === binding.scope
: context.state.scopes.get(parent) === binding.scope
) {
e.const_tag_invalid_reference(node, node.name);
} else {
break;
}
}
}
}
}
}

@ -359,16 +359,34 @@ export function client_component(analysis, options) {
if (dev) push_args.push(b.id(analysis.name));
let component_block = b.block([
store_init,
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body),
analysis.runes || !analysis.needs_context
? b.empty
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))
...group_binding_declarations
]);
if (analysis.instance.has_await) {
const body = b.block([
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body),
b.if(b.call('$.aborted'), b.return()),
.../** @type {ESTree.Statement[]} */ (template.body)
]);
component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true))));
} else {
component_block.body.push(
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body)
);
if (!analysis.runes && analysis.needs_context) {
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)));
}
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
}
if (analysis.needs_mutation_validation) {
component_block.body.unshift(
b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))
@ -389,41 +407,6 @@ export function client_component(analysis, options) {
analysis.uses_slots ||
analysis.slot_names.size > 0;
if (analysis.instance.has_await) {
const params = [b.id('$$anchor')];
if (should_inject_props) {
params.push(b.id('$$props'));
}
if (store_setup.length > 0) {
params.push(b.id('$$stores'));
}
const body = b.function_declaration(
b.id('$$body'),
params,
b.block([
b.var('$$unsuspend', b.call('$.suspend')),
...component_block.body,
b.if(b.call('$.aborted'), b.return()),
.../** @type {ESTree.Statement[]} */ (template.body),
b.stmt(b.call('$$unsuspend'))
]),
true
);
state.hoisted.push(body);
component_block = b.block([
b.var('fragment', b.call('$.comment')),
b.var('node', b.call('$.first_child', b.id('fragment'))),
store_init,
b.stmt(b.call(body.id, b.id('node'), ...params.slice(1))),
b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment')))
]);
} else {
component_block.body.unshift(store_init);
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
}
// trick esrap into including comments
component_block.loc = instance.loc;

@ -8,7 +8,12 @@ import { dev, is_ignored } from '../../../../state.js';
* @param {ComponentContext} context
*/
export function ForOfStatement(node, context) {
if (node.await && dev && !is_ignored(node, 'await_reactivity_loss')) {
if (
node.await &&
dev &&
!is_ignored(node, 'await_reactivity_loss') &&
context.state.options.experimental.async
) {
const left = /** @type {VariableDeclaration | Pattern} */ (context.visit(node.left));
const argument = /** @type {Expression} */ (context.visit(node.right));
const body = /** @type {Statement} */ (context.visit(node.body));

@ -51,7 +51,6 @@ export function Fragment(node, context) {
const has_await = context.state.init !== null && (node.metadata.has_await || false);
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
const unsuspend = b.id('$$unsuspend');
/** @type {Statement[]} */
const body = [];
@ -151,10 +150,6 @@ export function Fragment(node, context) {
}
}
if (has_await) {
body.push(b.var(unsuspend, b.call('$.suspend')));
}
body.push(...state.consts);
if (has_await) {
@ -182,8 +177,8 @@ export function Fragment(node, context) {
}
if (has_await) {
body.push(b.stmt(b.call(unsuspend)));
return b.block([b.stmt(b.call('$.async_body', b.arrow([], b.block(body), true)))]);
} else {
return b.block(body);
}
return b.block(body);
}

@ -39,40 +39,60 @@ export function SvelteBoundary(node, context) {
/** @type {Statement[]} */
const hoisted = [];
let has_const = false;
// const tags need to live inside the boundary, but might also be referenced in hoisted snippets.
// to resolve this we cheat: we duplicate const tags inside snippets
// We'll revert this behavior in the future, it was a mistake to allow this (Component snippets also don't do this).
for (const child of node.fragment.nodes) {
if (child.type === 'ConstTag') {
context.visit(child, { ...context.state, consts: const_tags });
has_const = true;
if (!context.state.options.experimental.async) {
context.visit(child, { ...context.state, consts: const_tags });
}
}
}
for (const child of node.fragment.nodes) {
if (child.type === 'ConstTag') {
if (context.state.options.experimental.async) {
nodes.push(child);
}
continue;
}
if (child.type === 'SnippetBlock') {
/** @type {Statement[]} */
const statements = [];
context.visit(child, { ...context.state, init: statements });
const snippet = /** @type {VariableDeclaration} */ (statements[0]);
const snippet_fn = dev
? // @ts-expect-error we know this shape is correct
snippet.declarations[0].init.arguments[1]
: snippet.declarations[0].init;
snippet_fn.body.body.unshift(
...const_tags.filter((node) => node.type === 'VariableDeclaration')
);
hoisted.push(snippet);
if (['failed', 'pending'].includes(child.expression.name)) {
props.properties.push(b.prop('init', child.expression, child.expression));
if (
context.state.options.experimental.async &&
has_const &&
!['failed', 'pending'].includes(child.expression.name)
) {
// we can't hoist snippets as they may reference const tags, so we just keep them in the fragment
nodes.push(child);
} else {
/** @type {Statement[]} */
const statements = [];
context.visit(child, { ...context.state, init: statements });
const snippet = /** @type {VariableDeclaration} */ (statements[0]);
const snippet_fn = dev
? // @ts-expect-error we know this shape is correct
snippet.declarations[0].init.arguments[1]
: snippet.declarations[0].init;
if (!context.state.options.experimental.async) {
snippet_fn.body.body.unshift(
...const_tags.filter((node) => node.type === 'VariableDeclaration')
);
}
if (['failed', 'pending'].includes(child.expression.name)) {
props.properties.push(b.prop('init', child.expression, child.expression));
}
hoisted.push(snippet);
}
continue;
@ -83,7 +103,9 @@ export function SvelteBoundary(node, context) {
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
block.body.unshift(...const_tags);
if (!context.state.options.experimental.async) {
block.body.unshift(...const_tags);
}
const boundary = b.stmt(
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))

@ -34,7 +34,7 @@ export class Memoizer {
}
apply() {
return [...this.#async, ...this.#sync].map((memo, i) => {
return [...this.#sync, ...this.#async].map((memo, i) => {
memo.id.name = `$${i}`;
return memo.id;
});

@ -23,6 +23,15 @@ export function is_element_node(node) {
return element_nodes.includes(node.type);
}
/**
* Returns true for all component-like nodes
* @param {AST.SvelteNode} node
* @returns {node is AST.Component | AST.SvelteComponent | AST.SvelteSelf}
*/
export function is_component_node(node) {
return ['Component', 'SvelteComponent', 'SvelteSelf'].includes(node.type);
}
/**
* @param {AST.RegularElement | AST.SvelteElement} node
* @returns {boolean}

@ -122,7 +122,7 @@ export class Binding {
/**
* Additional metadata, varies per binding type
* @type {null | { inside_rest?: boolean }}
* @type {null | { inside_rest?: boolean; is_template_declaration?: boolean }}
*/
metadata = null;
@ -1121,6 +1121,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
node.kind,
declarator.init
);
binding.metadata = { is_template_declaration: true };
bindings.push(binding);
}
}

@ -26,7 +26,7 @@ export function inspect(get_value, inspector = console.log) {
return;
}
var snap = snapshot(value, true);
var snap = snapshot(value, true, true);
untrack(() => {
inspector(initial ? 'init' : 'update', ...snap);
});

@ -6,6 +6,7 @@ import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { active_effect } from '../runtime.js';
import { async_mode_flag } from '../../flags/index.js';
import { TEXT_NODE, EFFECT_RAN } from '#client/constants';
import { eager_block_effects } from '../reactivity/batch.js';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@ -214,6 +215,7 @@ export function clear_text_content(node) {
*/
export function should_defer_append() {
if (!async_mode_flag) return false;
if (eager_block_effects !== null) return false;
var flags = /** @type {Effect} */ (active_effect).f;
return (flags & EFFECT_RAN) !== 0;

@ -99,6 +99,7 @@ export {
with_script
} from './dom/template.js';
export {
async_body,
for_await_track_reactivity_loss,
save,
track_reactivity_loss
@ -151,7 +152,8 @@ export {
untrack,
exclude_from_object,
deep_read,
deep_read_state
deep_read_state,
active_effect
} from './runtime.js';
export { validate_binding, validate_each_keys } from './validate.js';
export { raf } from './timing.js';
@ -176,3 +178,4 @@ export {
} from '../shared/validate.js';
export { strict_equals, equals } from './dev/equality.js';
export { log_if_contains_state } from './dev/console-log.js';
export { invoke_error_boundary } from './error-handling.js';

@ -93,9 +93,11 @@ export function proxy(value) {
/** Used in dev for $inspect.trace() */
var path = '';
let updating = false;
/** @param {string} new_path */
function update_path(new_path) {
if (updating) return;
updating = true;
path = new_path;
tag(version, `${path} version`);
@ -104,6 +106,7 @@ export function proxy(value) {
for (const [prop, source] of sources) {
tag(source, get_label(path, prop));
}
updating = false;
}
return new Proxy(/** @type {any} */ (value), {
@ -284,13 +287,13 @@ export function proxy(value) {
if (s === undefined) {
if (!has || get_descriptor(target, prop)?.writable) {
s = with_parent(() => source(undefined, stack));
set(s, proxy(value));
sources.set(prop, s);
if (DEV) {
tag(s, get_label(path, prop));
}
set(s, proxy(value));
sources.set(prop, s);
}
} else {
has = s.v !== UNINITIALIZED;

@ -11,7 +11,7 @@ import {
set_active_effect,
set_active_reaction
} from '../runtime.js';
import { current_batch } from './batch.js';
import { current_batch, suspend } from './batch.js';
import {
async_derived,
current_async_effect,
@ -19,6 +19,7 @@ import {
derived_safe_equal,
set_from_async_derived
} from './deriveds.js';
import { aborted } from './effects.js';
/**
*
@ -72,11 +73,13 @@ function capture() {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
var previous_component_context = component_context;
var previous_batch = current_batch;
return function restore() {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_component_context);
previous_batch?.activate();
if (DEV) {
set_from_async_derived(null);
@ -170,3 +173,21 @@ export function unset_context() {
set_component_context(null);
if (DEV) set_from_async_derived(null);
}
/**
* @param {() => Promise<void>} fn
*/
export async function async_body(fn) {
var unsuspend = suspend();
var active = /** @type {Effect} */ (active_effect);
try {
await fn();
} catch (error) {
if (!aborted(active)) {
invoke_error_boundary(error, active);
}
} finally {
unsuspend();
}
}

@ -76,8 +76,8 @@ let queued_root_effects = [];
let last_scheduled_effect = null;
let is_flushing = false;
let is_flushing_sync = false;
export class Batch {
/**
* The current values of any sources that are updated in this batch
@ -324,12 +324,12 @@ export class Batch {
if (!skip && effect.fn !== null) {
if (is_branch) {
effect.f ^= CLEAN;
} else if ((flags & EFFECT) !== 0) {
this.#effects.push(effect);
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
} else if ((flags & CLEAN) === 0) {
if ((flags & EFFECT) !== 0) {
this.#effects.push(effect);
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
} else if ((flags & ASYNC) !== 0) {
if ((flags & ASYNC) !== 0) {
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
effects.push(effect);
} else if (is_dirty(effect)) {
@ -623,6 +623,9 @@ function infinite_loop_guard() {
}
}
/** @type {Effect[] | null} */
export let eager_block_effects = null;
/**
* @param {Array<Effect>} effects
* @returns {void}
@ -637,7 +640,7 @@ function flush_queued_effects(effects) {
var effect = effects[i++];
if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
var n = current_batch ? current_batch.current.size : 0;
eager_block_effects = [];
update_effect(effect);
@ -658,21 +661,20 @@ function flush_queued_effects(effects) {
}
}
// if state is written in a user effect, abort and re-schedule, lest we run
// effects that should be removed as a result of the state change
if (
current_batch !== null &&
current_batch.current.size > n &&
(effect.f & USER_EFFECT) !== 0
) {
break;
if (eager_block_effects.length > 0) {
// TODO this feels incorrect! it gets the tests passing
old_values.clear();
for (const e of eager_block_effects) {
update_effect(e);
}
eager_block_effects = [];
}
}
}
while (i < length) {
schedule_effect(effects[i++]);
}
eager_block_effects = null;
}
/**
@ -715,6 +717,8 @@ export function suspend() {
if (!pending) {
batch.activate();
batch.decrement();
} else {
batch.deactivate();
}
unset_context();

@ -133,29 +133,40 @@ function create_effect(type, fn, sync, push = true) {
schedule_effect(effect);
}
// if an effect has no dependencies, no DOM and no teardown function,
// don't bother adding it to the effect tree
var inert =
sync &&
effect.deps === null &&
effect.first === null &&
effect.nodes_start === null &&
effect.teardown === null &&
(effect.f & EFFECT_PRESERVED) === 0;
if (!inert && push) {
if (parent !== null) {
push_effect(effect, parent);
}
if (push) {
/** @type {Effect | null} */
var e = effect;
// if we're in a derived, add the effect there too
// if an effect has already ran and doesn't need to be kept in the tree
// (because it won't re-run, has no DOM, and has no teardown etc)
// then we skip it and go to its child (if any)
if (
active_reaction !== null &&
(active_reaction.f & DERIVED) !== 0 &&
(type & ROOT_EFFECT) === 0
sync &&
e.deps === null &&
e.teardown === null &&
e.nodes_start === null &&
e.first === e.last && // either `null`, or a singular child
(e.f & EFFECT_PRESERVED) === 0
) {
var derived = /** @type {Derived} */ (active_reaction);
(derived.effects ??= []).push(effect);
e = e.first;
}
if (e !== null) {
e.parent = parent;
if (parent !== null) {
push_effect(e, parent);
}
// if we're in a derived, add the effect there too
if (
active_reaction !== null &&
(active_reaction.f & DERIVED) !== 0 &&
(type & ROOT_EFFECT) === 0
) {
var derived = /** @type {Derived} */ (active_reaction);
(derived.effects ??= []).push(e);
}
}
}
@ -242,7 +253,7 @@ export function inspect_effect(fn) {
*/
export function effect_root(fn) {
Batch.ensure();
const effect = create_effect(ROOT_EFFECT, fn, true);
const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true);
return () => {
destroy_effect(effect);
@ -256,7 +267,7 @@ export function effect_root(fn) {
*/
export function component_root(fn) {
Batch.ensure();
const effect = create_effect(ROOT_EFFECT, fn, true);
const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true);
return (options = {}) => {
return new Promise((fulfil) => {
@ -375,7 +386,7 @@ export function block(fn, flags = 0) {
* @param {boolean} [push]
*/
export function branch(fn, push = true) {
return create_effect(BRANCH_EFFECT, fn, true, push);
return create_effect(BRANCH_EFFECT | EFFECT_PRESERVED, fn, true, push);
}
/**
@ -648,7 +659,6 @@ function resume_children(effect, local) {
}
}
export function aborted() {
var effect = /** @type {Effect} */ (active_effect);
export function aborted(effect = /** @type {Effect} */ (active_effect)) {
return (effect.f & DESTROYED) !== 0;
}

@ -33,7 +33,7 @@ import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js';
import { Batch, schedule_effect } from './batch.js';
import { Batch, eager_block_effects, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
@ -334,6 +334,12 @@ function mark_reactions(signal, status) {
if ((flags & DERIVED) !== 0) {
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
} else if (not_dirty) {
if ((flags & BLOCK_EFFECT) !== 0) {
if (eager_block_effects !== null) {
eager_block_effects.push(/** @type {Effect} */ (reaction));
}
}
schedule_effect(/** @type {Effect} */ (reaction));
}
}

@ -15,14 +15,15 @@ const empty = [];
* @template T
* @param {T} value
* @param {boolean} [skip_warning]
* @param {boolean} [no_tojson]
* @returns {Snapshot<T>}
*/
export function snapshot(value, skip_warning = false) {
export function snapshot(value, skip_warning = false, no_tojson = false) {
if (DEV && !skip_warning) {
/** @type {string[]} */
const paths = [];
const copy = clone(value, new Map(), '', paths);
const copy = clone(value, new Map(), '', paths, null, no_tojson);
if (paths.length === 1 && paths[0] === '') {
// value could not be cloned
w.state_snapshot_uncloneable();
@ -40,7 +41,7 @@ export function snapshot(value, skip_warning = false) {
return copy;
}
return clone(value, new Map(), '', empty);
return clone(value, new Map(), '', empty, null, no_tojson);
}
/**
@ -49,10 +50,11 @@ export function snapshot(value, skip_warning = false) {
* @param {Map<T, Snapshot<T>>} cloned
* @param {string} path
* @param {string[]} paths
* @param {null | T} original The original value, if `value` was produced from a `toJSON` call
* @param {null | T} [original] The original value, if `value` was produced from a `toJSON` call
* @param {boolean} [no_tojson]
* @returns {Snapshot<T>}
*/
function clone(value, cloned, path, paths, original = null) {
function clone(value, cloned, path, paths, original = null, no_tojson = false) {
if (typeof value === 'object' && value !== null) {
var unwrapped = cloned.get(value);
if (unwrapped !== undefined) return unwrapped;
@ -71,7 +73,7 @@ function clone(value, cloned, path, paths, original = null) {
for (var i = 0; i < value.length; i += 1) {
var element = value[i];
if (i in value) {
copy[i] = clone(element, cloned, DEV ? `${path}[${i}]` : path, paths);
copy[i] = clone(element, cloned, DEV ? `${path}[${i}]` : path, paths, null, no_tojson);
}
}
@ -88,8 +90,15 @@ function clone(value, cloned, path, paths, original = null) {
}
for (var key in value) {
// @ts-expect-error
copy[key] = clone(value[key], cloned, DEV ? `${path}.${key}` : path, paths);
copy[key] = clone(
// @ts-expect-error
value[key],
cloned,
DEV ? `${path}.${key}` : path,
paths,
null,
no_tojson
);
}
return copy;
@ -99,7 +108,7 @@ function clone(value, cloned, path, paths, original = null) {
return /** @type {Snapshot<T>} */ (structuredClone(value));
}
if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function') {
if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function' && !no_tojson) {
return clone(
/** @type {T & { toJSON(): any } } */ (value).toJSON(),
cloned,

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

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
async: true,
error: {
code: 'const_tag_invalid_reference',
message: 'The `{@const foo = ...}` declaration is not available in this snippet ',
position: [376, 379]
}
});

@ -0,0 +1,32 @@
<svelte:options runes />
<!-- ok -->
<svelte:boundary>
{@const foo = 'bar'}
{#snippet other()}
{foo}
{/snippet}
{foo}
<svelte:boundary>
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
{#snippet failed()}
{@const foo = 'bar'}
{foo}
{/snippet}
</svelte:boundary>
<!-- error -->
<svelte:boundary>
{@const foo = 'bar'}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
async: true,
error: {
code: 'const_tag_invalid_reference',
message: 'The `{@const foo = ...}` declaration is not available in this snippet ',
position: [298, 301]
}
});

@ -0,0 +1,27 @@
<svelte:options runes />
<!-- ok -->
<Component>
{@const foo = 'bar'}
{foo}
<Component>
{#snippet prop()}
{foo}
{/snippet}
</Component>
{#snippet prop()}
{@const foo = 'bar'}
{foo}
{/snippet}
</Component>
<!-- error -->
<Component>
{@const foo = 'bar'}
{#snippet prop()}
{foo}
{/snippet}
</Component>

@ -5,6 +5,7 @@ import { suite, type BaseTest } from '../suite';
import { read_file } from '../helpers.js';
interface CompilerErrorTest extends BaseTest {
async?: boolean;
error: {
code: string;
message: string;
@ -29,7 +30,8 @@ const { test, run } = suite<CompilerErrorTest>((config, cwd) => {
try {
compile(read_file(`${cwd}/main.svelte`), {
generate: 'client'
generate: 'client',
experimental: { async: config.async ?? false }
});
} catch (e) {
const error = e as CompileError;

@ -7,6 +7,6 @@ export default test({
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, `<h1>Hello, world!</h1>`);
assert.htmlEqual(target.innerHTML, `<h1>Hello, world!</h1> 5 01234`);
}
});

@ -3,6 +3,8 @@
</script>
<svelte:boundary>
{@const number = await Promise.resolve(5)}
{#snippet pending()}
<h1>Loading...</h1>
{/snippet}
@ -10,6 +12,14 @@
{#snippet greet()}
{@const greeting = await `Hello, ${name}!`}
<h1>{greeting}</h1>
{number}
{#if number > 4}
{@const length = await number}
{#each { length }, index}
{@const i = await index}
{i}
{/each}
{/if}
{/snippet}
{@render greet()}

@ -0,0 +1,9 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, 'value');
}
});

@ -0,0 +1,9 @@
<script>
const value = await 'value';
</script>
{#snippet valueSnippet()}
{value}
{/snippet}
{@render valueSnippet()}

@ -0,0 +1,8 @@
<script>
import App from './app.svelte';
</script>
<svelte:boundary>
{#snippet pending()}
{/snippet}
<App />
</svelte:boundary>

@ -0,0 +1,9 @@
import { tick } from 'svelte';
import { ok, test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<p>foo bar</p>');
}
});

@ -0,0 +1,17 @@
<script>
function foo() {
return 'foo';
}
async function bar() {
return Promise.resolve('bar');
}
</script>
<svelte:boundary>
<p>{foo()} {await bar()}</p>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,9 @@
<script>
import { route } from "./main.svelte";
await new Promise(async (_, reject) => {
await Promise.resolve();
route.current = 'other'
route.reject = reject;
});
</script>

@ -0,0 +1,15 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>reject</button> <p>pending</p>`,
async test({ assert, target }) {
const [reject] = target.querySelectorAll('button');
await tick();
reject.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>reject</button> <p>route: other</p>');
}
});

@ -0,0 +1,18 @@
<script module>
import Child from './Child.svelte';
export let route = $state({ current: 'home' });
</script>
<button onclick={() => route.reject()}>reject</button>
<svelte:boundary>
{#if route.current === 'home'}
<Child />
{:else}
<p>route: {route.current}</p>
{/if}
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,7 @@
<script>
import { route } from "./main.svelte";
await new Promise(async (_, reject) => {
route.reject = reject;
});
</script>

@ -0,0 +1,14 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>reject</button> <p>pending</p>`,
async test({ assert, target }) {
const [reject] = target.querySelectorAll('button');
reject.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>reject</button> <p>failed</p>');
}
});

@ -0,0 +1,18 @@
<script module>
import Child from './Child.svelte';
export let route = $state({});
</script>
<button onclick={() => route.reject()}>reject</button>
<svelte:boundary>
<Child />
{#snippet pending()}
<p>pending</p>
{/snippet}
{#snippet failed()}
<p>failed</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,18 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
skip_async: true,
html: '<button></button><p>2</p>',
mode: ['client'],
test({ target, assert }) {
const btn = target.querySelector('button');
const p = target.querySelector('p');
flushSync(() => {
btn?.click();
});
assert.equal(p?.innerHTML, '4');
}
});

@ -0,0 +1,14 @@
<script>
import FlakyComponent from "./FlakyComponent.svelte";
let test=$state(1);
</script>
<button onclick={()=>test++}></button>
<svelte:boundary>
{@const double = test * 2}
{#snippet failed()}
<p>{double}</p>
{/snippet}
<FlakyComponent />
</svelte:boundary>

@ -2,7 +2,7 @@ import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: '<button></button><p>2</p>',
html: '<button>increment</button><p>2</p>',
mode: ['client'],
test({ target, assert }) {
const btn = target.querySelector('button');

@ -1,14 +1,10 @@
<script>
import FlakyComponent from "./FlakyComponent.svelte";
let test=$state(1);
let count = $state(1);
</script>
<button onclick={()=>test++}></button>
<button onclick={()=>count++}>increment</button>
<svelte:boundary>
{@const double = test * 2}
{#snippet failed()}
<p>{double}</p>
{/snippet}
<FlakyComponent />
</svelte:boundary>
{@const double = count * 2}
<p>{double}</p>
</svelte:boundary>

@ -0,0 +1,13 @@
<script>
let { children } = $props();
let inited = $state(false);
$effect(() => {
inited = true;
});
</script>
{#if inited}
<span>{@render children()}</span>
{/if}

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [button] = target.querySelectorAll('button');
assert.doesNotThrow(() => {
flushSync(() => button.click());
});
}
});

@ -0,0 +1,15 @@
<script>
import Child from './Child.svelte';
let show = $state(false);
</script>
<button onclick={() => show = !show}>
toggle
</button>
{#if show}
{#each { length: 1234 } as i}
<Child>{i}</Child>
{/each}
{/if}

@ -1,11 +1,12 @@
<script>
import B from './B.svelte';
let { boolean, closed } = $props();
let { boolean, closed, close } = $props();
// this runs after the effect in B, because child effects run first
$effect(() => {
console.log(boolean);
console.log({ boolean, closed });
});
</script>
<B {closed} />
<B {closed} {close} />

@ -1,7 +1,5 @@
<script>
import { close } from './Child.svelte';
let { closed } = $props();
let { closed, close } = $props();
$effect(() => {
if (closed) close();

@ -1,20 +0,0 @@
<script module>
let object = $state();
export function open() {
object = { boolean: true };
}
export function close() {
object = undefined;
}
</script>
<script>
let { children } = $props();
</script>
{#if object?.boolean}
<!-- error occurs here, this is executed when the if should already make it falsy -->
{@render children(object.boolean)}
{/if}

@ -8,6 +8,6 @@ export default test({
flushSync(() => open.click());
flushSync(() => close.click());
assert.deepEqual(logs, [true]);
assert.deepEqual(logs, [{ boolean: true, closed: false }]);
}
});

@ -1,6 +1,15 @@
<script>
import A from './A.svelte';
import Child, { open } from './Child.svelte';
let object = $state();
function open() {
object = { boolean: true };
}
function close() {
object = undefined;
}
let closed = $state(false);
</script>
@ -15,9 +24,6 @@
<hr>
<Child>
{#snippet children(boolean)}
<A {closed} {boolean} />
{/snippet}
</Child>
{#if object}
<A {closed} {close} boolean={object.boolean} />
{/if}

@ -1,9 +1,9 @@
<script>
import B from './B.svelte';
let { boolean, closed } = $props();
let { boolean, closed, close } = $props();
</script>
<span>{boolean}</span>
<span>{boolean} {closed}</span>
<B {closed} />
<B {closed} {close} />

@ -1,7 +1,5 @@
<script>
import { close } from './Child.svelte';
let { closed } = $props();
let { closed, close } = $props();
$effect.pre(() => {
if (closed) close();

@ -1,20 +0,0 @@
<script module>
let object = $state();
export function open() {
object = { nested: { boolean: true } };
}
export function close() {
object = undefined;
}
</script>
<script>
let { children } = $props();
</script>
{#if object?.nested}
<!-- error occurs here, this is executed when the if should already make it falsy -->
{@render children(object.nested)}
{/if}

@ -1,6 +1,15 @@
<script>
import A from './A.svelte';
import Child, { open } from './Child.svelte';
let object = $state();
function open() {
object = { boolean: true };
}
function close() {
object = undefined;
}
let closed = $state(false);
</script>
@ -15,8 +24,6 @@
<hr>
<Child>
{#snippet children(nested)}
<A {closed} boolean={nested.boolean} />
{/snippet}
</Child>
{#if object}
<A {close} {closed} boolean={object.boolean} />
{/if}

@ -1,21 +1,15 @@
<script>
class A {
toJSON(){
return {
a: this
}
constructor() {
this.a = this;
}
}
const state = $state(new A());
$inspect(state);
class B {
toJSON(){
return {
a: {
b: this
}
}
constructor() {
this.a = { b: this };
}
}
const state2 = $state(new B());

@ -0,0 +1,26 @@
import { test } from '../../test';
import { normalise_trace_logs } from '../../../helpers.js';
export default test({
compileOptions: {
dev: true
},
test({ assert, logs }) {
const files = { id: 1, items: [{ id: 2, items: [{ id: 3 }, { id: 4 }] }] };
// @ts-expect-error
files.items[0].parent = files;
assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'test (main.svelte:5:4)' },
{ log: '$state', highlighted: true },
{ log: 'filesState.files', highlighted: false },
{ log: files },
{ log: '$state', highlighted: true },
{ log: 'filesState.files.items[0].parent.items', highlighted: false },
{ log: files.items },
{ log: '$state', highlighted: true },
{ log: 'filesState.files.items[0].parent.items[0]', highlighted: false },
{ log: files.items[0] }
]);
}
});

@ -0,0 +1,10 @@
<script>
const filesState = $state({ files: {} });
let nodes = { id: 1, items: [{ id: 2, items: [{ id: 3 }, { id: 4 }] }] };
filesState.files = nodes;
function test() {
$inspect.trace();
filesState.files.items[0].parent = filesState.files;
}
$effect(test);
</script>

@ -4,8 +4,6 @@
<svelte:boundary>
{@const x = a}
{#snippet failed()}
{x}
{/snippet}
{x}
<FlakyComponent />
</svelte:boundary>
</svelte:boundary>

Loading…
Cancel
Save