diff --git a/.changeset/flat-points-kick.md b/.changeset/flat-points-kick.md new file mode 100644 index 0000000000..f9c8b8762c --- /dev/null +++ b/.changeset/flat-points-kick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reset `is_flushing` if `flushSync` is called and there's no scheduled effect diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md index e502b7921a..c24c1febee 100644 --- a/documentation/docs/07-misc/07-v5-migration-guide.md +++ b/documentation/docs/07-misc/07-v5-migration-guide.md @@ -833,9 +833,9 @@ Svelte 5 is more strict about the HTML structure and will throw a compiler error Assignments to destructured parts of a `@const` declaration are no longer allowed. It was an oversight that this was ever allowed. -### :is(...) and :where(...) are scoped +### :is(...), :has(...), and :where(...) are scoped -Previously, Svelte did not analyse selectors inside `:is(...)` and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:where(...)` selectors. +Previously, Svelte did not analyse selectors inside `:is(...)`, `:has(...)`, and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:has(...)/:where(...)` selectors. When using Tailwind's `@apply` directive, add a `:global` selector to preserve rules that use Tailwind-generated `:is(...)` selectors: diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index a7c08cc1aa..506f04c1c4 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,47 @@ # svelte +## 5.33.18 + +### Patch Changes + +- chore: bump `esrap` dependency ([#16106](https://github.com/sveltejs/svelte/pull/16106)) + +- fix: destructuring state in ssr ([#16102](https://github.com/sveltejs/svelte/pull/16102)) + +## 5.33.17 + +### Patch Changes + +- chore: update acorn parser `ecmaVersion` to parse import attributes ([#16098](https://github.com/sveltejs/svelte/pull/16098)) + +## 5.33.16 + +### Patch Changes + +- fix: visit expression when destructuring state declarations ([#16081](https://github.com/sveltejs/svelte/pull/16081)) + +- fix: move xmlns attribute from SVGAttributes to to DOMAttributes ([#16080](https://github.com/sveltejs/svelte/pull/16080)) + +## 5.33.15 + +### Patch Changes + +- fix: invoke parent boundary of deriveds that throw ([#16091](https://github.com/sveltejs/svelte/pull/16091)) + +## 5.33.14 + +### Patch Changes + +- Revert "feat: enable TS autocomplete for Svelte HTML element definitions" ([#16063](https://github.com/sveltejs/svelte/pull/16063)) + +- fix: destructuring snippet arguments ([#16068](https://github.com/sveltejs/svelte/pull/16068)) + +## 5.33.13 + +### Patch Changes + +- fix: avoid recursion error in `EachBlock` visitor ([#16058](https://github.com/sveltejs/svelte/pull/16058)) + ## 5.33.12 ### Patch Changes diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 0172b0e358..8a1bdd0e6d 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -463,6 +463,8 @@ export interface DOMAttributes { 'on:fullscreenerror'?: EventHandler | undefined | null; onfullscreenerror?: EventHandler | undefined | null; onfullscreenerrorcapture?: EventHandler | undefined | null; + + xmlns?: string | undefined | null; } // All the WAI-ARIA 1.1 attributes from https://www.w3.org/TR/wai-aria-1.1/ @@ -1809,7 +1811,6 @@ export interface SVGAttributes extends AriaAttributes, DO 'xlink:type'?: string | undefined | null; 'xml:base'?: string | undefined | null; 'xml:lang'?: string | undefined | null; - xmlns?: string | undefined | null; 'xmlns:xlink'?: string | undefined | null; 'xml:space'?: string | undefined | null; y1?: number | string | undefined | null; @@ -2066,7 +2067,7 @@ export interface SvelteHTMLElements { failed?: import('svelte').Snippet<[error: unknown, reset: () => void]>; }; - [name: string & {}]: { [name: string]: any }; + [name: string]: { [name: string]: any }; } export type ClassValue = string | import('clsx').ClassArray | import('clsx').ClassDictionary; diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 866a6ecc61..0758158a40 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.33.12", + "version": "5.33.18", "type": "module", "types": "./types/index.d.ts", "engines": { @@ -171,7 +171,7 @@ "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", - "esrap": "^1.4.6", + "esrap": "^1.4.8", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 36f7688c49..26a09abb66 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -36,7 +36,7 @@ export function parse(source, typescript, is_script) { ast = parser.parse(source, { onComment, sourceType: 'module', - ecmaVersion: 13, + ecmaVersion: 16, locations: true }); } finally { @@ -64,7 +64,7 @@ export function parse_expression_at(source, typescript, index) { const ast = parser.parseExpressionAt(source, index, { onComment, sourceType: 'module', - ecmaVersion: 13, + ecmaVersion: 16, locations: true }); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js index 0ebfa563cf..e6a83921b1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js @@ -77,6 +77,9 @@ export function EachBlock(node, context) { * @returns {void} */ function collect_transitive_dependencies(binding, bindings) { + if (bindings.has(binding)) { + return; + } bindings.add(binding); if (binding.kind === 'legacy_reactive') { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index 0809aa21b2..203cf62b37 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -57,7 +57,7 @@ export function SnippetBlock(node, context) { for (const path of paths) { const name = /** @type {Identifier} */ (path.node).name; const needs_derived = path.has_default_value; // to ensure that default value is only called once - const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression))); + const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression, child_state))); declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index ec1c650814..1480648ce5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Statement, Expression } from 'estree' */ +/** @import { BlockStatement, Statement, Expression, FunctionDeclaration, VariableDeclaration, ArrowFunctionExpression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; @@ -34,62 +34,61 @@ export function SvelteBoundary(node, context) { const nodes = []; /** @type {Statement[]} */ - const external_statements = []; + const const_tags = []; /** @type {Statement[]} */ - const internal_statements = []; + const hoisted = []; - const snippets_visits = []; + // 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 + for (const child of node.fragment.nodes) { + if (child.type === 'ConstTag') { + context.visit(child, { ...context.state, init: const_tags }); + } + } - // Capture the `failed` implicit snippet prop for (const child of node.fragment.nodes) { - if (child.type === 'SnippetBlock' && child.expression.name === 'failed') { - // we need to delay the visit of the snippets in case they access a ConstTag that is declared - // after the snippets so that the visitor for the const tag can be updated - snippets_visits.push(() => { - /** @type {Statement[]} */ - const init = []; - context.visit(child, { ...context.state, init }); - props.properties.push(b.prop('init', child.expression, child.expression)); - external_statements.push(...init); - }); - } else if (child.type === 'ConstTag') { + if (child.type === 'ConstTag') { + continue; + } + + if (child.type === 'SnippetBlock') { /** @type {Statement[]} */ - const init = []; - context.visit(child, { ...context.state, init }); - - if (dev) { - // In dev we must separate the declarations from the code - // that eagerly evaluate the expression... - for (const statement of init) { - if (statement.type === 'VariableDeclaration') { - external_statements.push(statement); - } else { - internal_statements.push(statement); - } - } - } else { - external_statements.push(...init); + 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 (child.expression.name === 'failed') { + props.properties.push(b.prop('init', child.expression, child.expression)); } - } else { - nodes.push(child); + + continue; } - } - snippets_visits.forEach((visit) => visit()); + nodes.push(child); + } const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes })); - if (dev && internal_statements.length) { - block.body.unshift(...internal_statements); - } + block.body.unshift(...const_tags); const boundary = b.stmt( b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block)) ); context.state.template.push_comment(); - context.state.init.push( - external_statements.length > 0 ? b.block([...external_statements, boundary]) : boundary - ); + context.state.init.push(hoisted.length > 0 ? b.block([...hoisted, boundary]) : boundary); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 4e5ab9bb95..13869229b5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -164,7 +164,7 @@ export function VariableDeclaration(node, context) { const { inserts, paths } = extract_paths(declarator.id, tmp); declarations.push( - b.declarator(tmp, value), + b.declarator(tmp, /** @type {Expression} */ (context.visit(value))), ...inserts.map(({ id, value }) => { id.name = context.state.scope.generate('$$array'); context.state.transform[id.name] = { read: get_value }; diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js index 1f0e6be77c..7c94595147 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js @@ -197,9 +197,13 @@ function create_state_declarators(declarator, scope, value) { } const tmp = b.id(scope.generate('tmp')); - const { paths } = extract_paths(declarator.id, tmp); + const { paths, inserts } = extract_paths(declarator.id, tmp); return [ b.declarator(tmp, value), // TODO inject declarator for opts, so we can use it below + ...inserts.map(({ id, value }) => { + id.name = scope.generate('$$array'); + return b.declarator(id, value); + }), ...paths.map((path) => { const value = path.expression; return b.declarator(path.node, value); diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 53060017b9..d7a53da1d1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -2,14 +2,13 @@ import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '#client/constants'; import { component_context, set_component_context } from '../../context.js'; +import { invoke_error_boundary } from '../../error-handling.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; import { active_effect, active_reaction, - handle_error, set_active_effect, - set_active_reaction, - reset_is_throwing_error + set_active_reaction } from '../../runtime.js'; import { hydrate_next, @@ -80,11 +79,17 @@ export function boundary(node, props, boundary_fn) { with_boundary(boundary, () => { is_creating_fallback = false; boundary_effect = branch(() => boundary_fn(anchor)); - reset_is_throwing_error(); }); }; - onerror?.(error, reset); + var previous_reaction = active_reaction; + + try { + set_active_reaction(null); + onerror?.(error, reset); + } finally { + set_active_reaction(previous_reaction); + } if (boundary_effect) { destroy_effect(boundary_effect); @@ -109,10 +114,9 @@ export function boundary(node, props, boundary_fn) { ); }); } catch (error) { - handle_error(error, boundary, null, boundary.ctx); + invoke_error_boundary(error, /** @type {Effect} */ (boundary.parent)); } - reset_is_throwing_error(); is_creating_fallback = false; }); }); @@ -124,7 +128,6 @@ export function boundary(node, props, boundary_fn) { } boundary_effect = branch(() => boundary_fn(anchor)); - reset_is_throwing_error(); }, EFFECT_TRANSPARENT | BOUNDARY_EFFECT); if (hydrating) { diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js new file mode 100644 index 0000000000..aeec1d8b47 --- /dev/null +++ b/packages/svelte/src/internal/client/error-handling.js @@ -0,0 +1,88 @@ +/** @import { Effect } from '#client' */ +import { DEV } from 'esm-env'; +import { FILENAME } from '../../constants.js'; +import { is_firefox } from './dom/operations.js'; +import { BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js'; +import { define_property } from '../shared/utils.js'; +import { active_effect } from './runtime.js'; + +/** + * @param {unknown} error + */ +export function handle_error(error) { + var effect = /** @type {Effect} */ (active_effect); + + if (DEV && error instanceof Error) { + adjust_error(error, effect); + } + + if ((effect.f & EFFECT_RAN) === 0) { + // 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) { + throw error; + } + + // @ts-expect-error + effect.fn(error); + } else { + // otherwise we bubble up the effect tree ourselves + invoke_error_boundary(error, effect); + } +} + +/** + * @param {unknown} error + * @param {Effect | null} effect + */ +export function invoke_error_boundary(error, effect) { + while (effect !== null) { + if ((effect.f & BOUNDARY_EFFECT) !== 0) { + try { + // @ts-expect-error + effect.fn(error); + return; + } catch {} + } + + effect = effect.parent; + } + + throw error; +} + +/** @type {WeakSet} */ +const adjusted_errors = new WeakSet(); + +/** + * Add useful information to the error message/stack in development + * @param {Error} error + * @param {Effect} effect + */ +function adjust_error(error, effect) { + if (adjusted_errors.has(error)) return; + adjusted_errors.add(error); + + var indent = is_firefox ? ' ' : '\t'; + var component_stack = `\n${indent}in ${effect.fn?.name || ''}`; + var context = effect.ctx; + + while (context !== null) { + component_stack += `\n${indent}in ${context.function?.[FILENAME].split('/').pop()}`; + context = context.p; + } + + define_property(error, 'message', { + value: error.message + `\n${component_stack}\n` + }); + + if (error.stack) { + // Filter out internal modules + define_property(error, 'stack', { + value: error.stack + .split('\n') + .filter((line) => !line.includes('svelte/src/internal')) + .join('\n') + }); + } +} diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 36be1ecd04..54994b9bd1 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -332,16 +332,23 @@ export function render_effect(fn) { * @returns {Effect} */ export function template_effect(fn, thunks = [], d = derived) { - const deriveds = thunks.map(d); - const effect = () => fn(...deriveds.map(get)); - if (DEV) { - define_property(effect, 'name', { - value: '{expression}' + // wrap the effect so that we can decorate stack trace with `in {expression}` + // (TODO maybe there's a better approach?) + return render_effect(() => { + var outer = /** @type {Effect} */ (active_effect); + var inner = () => fn(...deriveds.map(get)); + + define_property(outer.fn, 'name', { value: '{expression}' }); + define_property(inner, 'name', { value: '{expression}' }); + + const deriveds = thunks.map(d); + block(inner); }); } - return block(effect); + const deriveds = thunks.map(d); + return block(() => fn(...deriveds.map(get))); } /** @@ -426,7 +433,11 @@ export function destroy_block_effect_children(signal) { export function destroy_effect(effect, remove_dom = true) { var removed = false; - if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes_start !== null) { + if ( + (remove_dom || (effect.f & HEAD_EFFECT) !== 0) && + effect.nodes_start !== null && + effect.nodes_end !== null + ) { remove_effect_dom(effect.nodes_start, /** @type {TemplateNode} */ (effect.nodes_end)); removed = true; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index d790c0ad14..51402ac88c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,4 +1,4 @@ -/** @import { ComponentContext, Derived, Effect, Reaction, Signal, Source, Value } from '#client' */ +/** @import { Derived, Effect, Reaction, Signal, Source, Value } from '#client' */ import { DEV } from 'esm-env'; import { define_property, get_descriptors, get_prototype_of, index_of } from '../shared/utils.js'; import { @@ -22,14 +22,13 @@ import { ROOT_EFFECT, LEGACY_DERIVED_PROP, DISCONNECTED, - BOUNDARY_EFFECT, EFFECT_IS_UPDATING } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; -import { FILENAME } from '../../constants.js'; + import { tracing_mode_flag } from '../flags/index.js'; import { tracing_expressions, get_stack } from './dev/tracing.js'; import { @@ -39,12 +38,7 @@ import { set_component_context, set_dev_current_component_function } from './context.js'; -import { is_firefox } from './dom/operations.js'; - -// Used for DEV time error handling -/** @param {WeakSet} value */ -const handled_errors = new WeakSet(); -let is_throwing_error = false; +import { handle_error, invoke_error_boundary } from './error-handling.js'; let is_flushing = false; @@ -227,131 +221,6 @@ export function check_dirtiness(reaction) { return false; } -/** - * @param {unknown} error - * @param {Effect} effect - */ -function propagate_error(error, effect) { - /** @type {Effect | null} */ - var current = effect; - - while (current !== null) { - if ((current.f & BOUNDARY_EFFECT) !== 0) { - try { - // @ts-expect-error - current.fn(error); - return; - } catch { - // Remove boundary flag from effect - current.f ^= BOUNDARY_EFFECT; - } - } - - current = current.parent; - } - - is_throwing_error = false; - throw error; -} - -/** - * @param {Effect} effect - */ -function should_rethrow_error(effect) { - return ( - (effect.f & DESTROYED) === 0 && - (effect.parent === null || (effect.parent.f & BOUNDARY_EFFECT) === 0) - ); -} - -export function reset_is_throwing_error() { - is_throwing_error = false; -} - -/** - * @param {unknown} error - * @param {Effect} effect - * @param {Effect | null} previous_effect - * @param {ComponentContext | null} component_context - */ -export function handle_error(error, effect, previous_effect, component_context) { - if (is_throwing_error) { - if (previous_effect === null) { - is_throwing_error = false; - } - - if (should_rethrow_error(effect)) { - throw error; - } - - return; - } - - if (previous_effect !== null) { - is_throwing_error = true; - } - - if (DEV && component_context !== null && error instanceof Error && !handled_errors.has(error)) { - handled_errors.add(error); - - const component_stack = []; - - const effect_name = effect.fn?.name; - - if (effect_name) { - component_stack.push(effect_name); - } - - /** @type {ComponentContext | null} */ - let current_context = component_context; - - while (current_context !== null) { - /** @type {string} */ - var filename = current_context.function?.[FILENAME]; - - if (filename) { - const file = filename.split('/').pop(); - component_stack.push(file); - } - - current_context = current_context.p; - } - - const indent = is_firefox ? ' ' : '\t'; - define_property(error, 'message', { - value: - error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n` - }); - define_property(error, 'component_stack', { - value: component_stack - }); - - const stack = error.stack; - - // Filter out internal files from callstack - if (stack) { - const lines = stack.split('\n'); - const new_lines = []; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes('svelte/src/internal')) { - continue; - } - new_lines.push(line); - } - define_property(error, 'stack', { - value: new_lines.join('\n') - }); - } - } - - propagate_error(error, effect); - - if (should_rethrow_error(effect)) { - throw error; - } -} - /** * @param {Value} signal * @param {Effect} effect @@ -379,11 +248,7 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true) } } -/** - * @template V - * @param {Reaction} reaction - * @returns {V} - */ +/** @param {Reaction} reaction */ export function update_reaction(reaction) { var previous_deps = new_deps; var previous_skipped_deps = skipped_deps; @@ -473,6 +338,8 @@ export function update_reaction(reaction) { } return result; + } catch (error) { + handle_error(error); } finally { new_deps = previous_deps; skipped_deps = previous_skipped_deps; @@ -558,7 +425,6 @@ export function update_effect(effect) { set_signal_status(effect, CLEAN); var previous_effect = active_effect; - var previous_component_context = component_context; var was_updating_effect = is_updating_effect; active_effect = effect; @@ -601,8 +467,6 @@ export function update_effect(effect) { if (DEV) { dev_effect_stack.push(effect); } - } catch (error) { - handle_error(error, effect, previous_effect, previous_component_context || effect.ctx); } finally { is_updating_effect = was_updating_effect; active_effect = previous_effect; @@ -637,14 +501,14 @@ function infinite_loop_guard() { if (last_scheduled_effect !== null) { if (DEV) { try { - handle_error(error, last_scheduled_effect, null, null); + invoke_error_boundary(error, last_scheduled_effect); } catch (e) { // Only log the effect stack if the error is re-thrown log_effect_stack(); throw e; } } else { - handle_error(error, last_scheduled_effect, null, null); + invoke_error_boundary(error, last_scheduled_effect); } } else { if (DEV) { @@ -701,27 +565,23 @@ function flush_queued_effects(effects) { var effect = effects[i]; if ((effect.f & (DESTROYED | INERT)) === 0) { - try { - if (check_dirtiness(effect)) { - update_effect(effect); - - // Effects with no dependencies or teardown do not get added to the effect tree. - // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we - // don't know if we need to keep them until they are executed. Doing the check - // here (rather than in `update_effect`) allows us to skip the work for - // immediate effects. - if (effect.deps === null && effect.first === null && effect.nodes_start === null) { - if (effect.teardown === null) { - // remove this effect from the graph - unlink_effect(effect); - } else { - // keep the effect in the graph, but free up some memory - effect.fn = null; - } + if (check_dirtiness(effect)) { + update_effect(effect); + + // Effects with no dependencies or teardown do not get added to the effect tree. + // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we + // don't know if we need to keep them until they are executed. Doing the check + // here (rather than in `update_effect`) allows us to skip the work for + // immediate effects. + if (effect.deps === null && effect.first === null && effect.nodes_start === null) { + if (effect.teardown === null) { + // remove this effect from the graph + unlink_effect(effect); + } else { + // keep the effect in the graph, but free up some memory + effect.fn = null; } } - } catch (error) { - handle_error(error, effect, null, effect.ctx); } } } @@ -780,12 +640,8 @@ function process_effects(root) { } else if (is_branch) { effect.f ^= CLEAN; } else { - try { - if (check_dirtiness(effect)) { - update_effect(effect); - } - } catch (error) { - handle_error(error, effect, null, effect.ctx); + if (check_dirtiness(effect)) { + update_effect(effect); } } @@ -832,6 +688,13 @@ export function flushSync(fn) { flush_tasks(); if (queued_root_effects.length === 0) { + // this would be reset in `flush_queued_root_effects` but since we are early returning here, + // we need to reset it here as well in case the first time there's 0 queued root effects + is_flushing = false; + last_scheduled_effect = null; + if (DEV) { + dev_effect_stack = []; + } return /** @type {T} */ (result); } diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 555ca54282..2539247e01 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.33.12'; +export const VERSION = '5.33.18'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/svelte-html.d.ts b/packages/svelte/svelte-html.d.ts index 6e37cc93e8..5042eaa4b8 100644 --- a/packages/svelte/svelte-html.d.ts +++ b/packages/svelte/svelte-html.d.ts @@ -8,7 +8,7 @@ import * as svelteElements from './elements.js'; /** * @internal do not use */ -type HTMLProps = Omit< +type HTMLProps = Omit< import('./elements.js').SvelteHTMLElements[Property], keyof Override > & @@ -250,7 +250,7 @@ declare global { }; // don't type svelte:options, it would override the types in svelte/elements and it isn't extendable anyway - [name: string & {}]: { [name: string]: any }; + [name: string]: { [name: string]: any }; } } } diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js index 4c9e2a7253..6948d8db32 100644 --- a/packages/svelte/tests/html_equal.js +++ b/packages/svelte/tests/html_equal.js @@ -1,8 +1,20 @@ import { assert } from 'vitest'; -/** @param {Element} node */ -function clean_children(node) { +/** + * @param {Element} node + * @param {{ preserveComments: boolean }} opts + */ +function clean_children(node, opts) { let previous = null; + let has_element_children = false; + let template = + node.nodeName === 'TEMPLATE' ? /** @type {HTMLTemplateElement} */ (node) : undefined; + + if (template) { + const div = document.createElement('div'); + div.append(template.content); + node = div; + } // sort attributes const attributes = Array.from(node.attributes).sort((a, b) => { @@ -14,6 +26,11 @@ function clean_children(node) { }); attributes.forEach((attr) => { + // Strip out the special onload/onerror hydration events from the test output + if ((attr.name === 'onload' || attr.name === 'onerror') && attr.value === 'this.__e=event') { + return; + } + node.setAttribute(attr.name, attr.value); }); @@ -27,24 +44,43 @@ function clean_children(node) { node.tagName !== 'tspan' ) { node.removeChild(child); + continue; } - text.data = text.data.replace(/[ \t\n\r\f]+/g, '\n'); + text.data = text.data.replace(/[^\S]+/g, ' '); if (previous && previous.nodeType === 3) { const prev = /** @type {Text} */ (previous); prev.data += text.data; - prev.data = prev.data.replace(/[ \t\n\r\f]+/g, '\n'); - node.removeChild(text); + text = prev; + text.data = text.data.replace(/[^\S]+/g, ' '); + + continue; } - } else if (child.nodeType === 8) { + } + + if (child.nodeType === 8 && !opts.preserveComments) { // comment - // do nothing - } else { - clean_children(/** @type {Element} */ (child)); + child.remove(); + continue; + } + + // add newlines for better readability and potentially recurse into children + if (child.nodeType === 1 || child.nodeType === 8) { + if (previous?.nodeType === 3) { + const prev = /** @type {Text} */ (previous); + prev.data = prev.data.replace(/^[^\S]+$/, '\n'); + } else if (previous?.nodeType === 1 || previous?.nodeType === 8) { + node.insertBefore(document.createTextNode('\n'), child); + } + + if (child.nodeType === 1) { + has_element_children = true; + clean_children(/** @type {Element} */ (child), opts); + } } previous = child; @@ -53,37 +89,36 @@ function clean_children(node) { // collapse whitespace if (node.firstChild && node.firstChild.nodeType === 3) { const text = /** @type {Text} */ (node.firstChild); - text.data = text.data.replace(/^[ \t\n\r\f]+/, ''); - if (!text.data.length) node.removeChild(text); + text.data = text.data.trimStart(); } if (node.lastChild && node.lastChild.nodeType === 3) { const text = /** @type {Text} */ (node.lastChild); - text.data = text.data.replace(/[ \t\n\r\f]+$/, ''); - if (!text.data.length) node.removeChild(text); + text.data = text.data.trimEnd(); + } + + // indent code for better readability + if (has_element_children && node.parentNode) { + node.innerHTML = `\n\ ${node.innerHTML.replace(/\n/g, '\n ')}\n`; + } + + if (template) { + template.innerHTML = node.innerHTML; } } /** * @param {Window} window * @param {string} html - * @param {{ removeDataSvelte?: boolean, preserveComments?: boolean }} param2 + * @param {{ preserveComments?: boolean }} opts */ -export function normalize_html( - window, - html, - { removeDataSvelte = false, preserveComments = false } -) { +export function normalize_html(window, html, { preserveComments = false } = {}) { try { const node = window.document.createElement('div'); - node.innerHTML = html - .replace(/()/g, preserveComments ? '$1' : '') - .replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1') - .replace(/>[ \t\n\r\f]+<') - // Strip out the special onload/onerror hydration events from the test output - .replace(/\s?onerror="this.__e=event"|\s?onload="this.__e=event"/g, '') - .trim(); - clean_children(node); + + node.innerHTML = html.trim(); + clean_children(node, { preserveComments }); + return node.innerHTML; } catch (err) { throw new Error(`Failed to normalize HTML:\n${html}\nCause: ${err}`); @@ -99,67 +134,52 @@ export function normalize_new_line(html) { } /** - * @param {{ removeDataSvelte?: boolean }} options + * @param {string} actual + * @param {string} expected + * @param {string} [message] */ -export function setup_html_equal(options = {}) { - /** - * @param {string} actual - * @param {string} expected - * @param {string} [message] - */ - const assert_html_equal = (actual, expected, message) => { - try { - assert.deepEqual( - normalize_html(window, actual, options), - normalize_html(window, expected, options), - message - ); - } catch (e) { - if (Error.captureStackTrace) - Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal); - throw e; - } - }; - - /** - * - * @param {string} actual - * @param {string} expected - * @param {{ preserveComments?: boolean, withoutNormalizeHtml?: boolean }} param2 - * @param {string} [message] - */ - const assert_html_equal_with_options = ( - actual, - expected, - { preserveComments, withoutNormalizeHtml }, - message - ) => { - try { - assert.deepEqual( - withoutNormalizeHtml - ? normalize_new_line(actual.trim()) - .replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1') - .replace(/()/g, preserveComments !== false ? '$1' : '') - : normalize_html(window, actual.trim(), { ...options, preserveComments }), - withoutNormalizeHtml - ? normalize_new_line(expected.trim()) - .replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1') - .replace(/()/g, preserveComments !== false ? '$1' : '') - : normalize_html(window, expected.trim(), { ...options, preserveComments }), - message - ); - } catch (e) { - if (Error.captureStackTrace) - Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal_with_options); - throw e; - } - }; - - return { - assert_html_equal, - assert_html_equal_with_options - }; -} +export const assert_html_equal = (actual, expected, message) => { + try { + assert.deepEqual(normalize_html(window, actual), normalize_html(window, expected), message); + } catch (e) { + if (Error.captureStackTrace) + Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal); + throw e; + } +}; -// Common case without options -export const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal(); +/** + * + * @param {string} actual + * @param {string} expected + * @param {{ preserveComments?: boolean, withoutNormalizeHtml?: boolean }} param2 + * @param {string} [message] + */ +export const assert_html_equal_with_options = ( + actual, + expected, + { preserveComments, withoutNormalizeHtml }, + message +) => { + try { + assert.deepEqual( + withoutNormalizeHtml + ? normalize_new_line(actual.trim()).replace( + /()/g, + preserveComments !== false ? '$1' : '' + ) + : normalize_html(window, actual.trim(), { preserveComments }), + withoutNormalizeHtml + ? normalize_new_line(expected.trim()).replace( + /()/g, + preserveComments !== false ? '$1' : '' + ) + : normalize_html(window, expected.trim(), { preserveComments }), + message + ); + } catch (e) { + if (Error.captureStackTrace) + Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal_with_options); + throw e; + } +}; diff --git a/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json b/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json index 3dad9bb4e5..c6af77a47b 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json @@ -15,16 +15,16 @@ "end": 20, "type": "Action", "name": "autofocus", - "modifiers": [], - "expression": null + "expression": null, + "modifiers": [] }, { "start": 21, "end": 34, "type": "Action", "name": "autofocus", - "modifiers": [], - "expression": null + "expression": null, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json b/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json index 66ce187c62..a10d4eccf0 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json @@ -15,7 +15,6 @@ "end": 39, "type": "Action", "name": "tooltip", - "modifiers": [], "expression": { "type": "CallExpression", "start": 21, @@ -66,7 +65,8 @@ } ], "optional": false - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json b/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json index 39a6f5f647..e9a3e7e5da 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json @@ -15,7 +15,6 @@ "end": 28, "type": "Action", "name": "tooltip", - "modifiers": [], "expression": { "type": "Identifier", "start": 20, @@ -31,7 +30,8 @@ } }, "name": "message" - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json b/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json index 94c60b701a..94b60b9e5d 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json @@ -15,7 +15,6 @@ "end": 36, "type": "Action", "name": "tooltip", - "modifiers": [], "expression": { "type": "Literal", "start": 21, @@ -32,7 +31,8 @@ }, "value": "tooltip msg", "raw": "'tooltip msg'" - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/action/output.json b/packages/svelte/tests/parser-legacy/samples/action/output.json index d72bf7db10..f241c81a93 100644 --- a/packages/svelte/tests/parser-legacy/samples/action/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action/output.json @@ -15,8 +15,8 @@ "end": 20, "type": "Action", "name": "autofocus", - "modifiers": [], - "expression": null + "expression": null, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/animation/output.json b/packages/svelte/tests/parser-legacy/samples/animation/output.json index 0d82cb2bb9..bf4b43b875 100644 --- a/packages/svelte/tests/parser-legacy/samples/animation/output.json +++ b/packages/svelte/tests/parser-legacy/samples/animation/output.json @@ -20,8 +20,8 @@ "end": 50, "type": "Animation", "name": "flip", - "modifiers": [], - "expression": null + "expression": null, + "modifiers": [] } ], "children": [ diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json index 9efe9acf8d..3cd54b6647 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json @@ -15,7 +15,6 @@ "end": 22, "type": "Class", "name": "foo", - "modifiers": [], "expression": { "type": "Identifier", "start": 16, @@ -31,7 +30,8 @@ } }, "name": "isFoo" - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json index 4d3a291808..2e45184be9 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json @@ -15,7 +15,6 @@ "end": 23, "type": "EventHandler", "name": "click", - "modifiers": [], "expression": { "type": "Identifier", "start": 19, @@ -31,7 +30,8 @@ } }, "name": "foo" - } + }, + "modifiers": [] } ], "children": [ diff --git a/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json b/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json index 6720146297..4289245705 100644 --- a/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json +++ b/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json @@ -22,13 +22,13 @@ "end": 46, "type": "Binding", "name": "foo", - "modifiers": [], "expression": { "start": 43, "end": 46, "type": "Identifier", "name": "foo" - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/binding/output.json b/packages/svelte/tests/parser-legacy/samples/binding/output.json index 4ce069bd37..5256ede7bb 100644 --- a/packages/svelte/tests/parser-legacy/samples/binding/output.json +++ b/packages/svelte/tests/parser-legacy/samples/binding/output.json @@ -22,7 +22,6 @@ "end": 55, "type": "Binding", "name": "value", - "modifiers": [], "expression": { "type": "Identifier", "start": 50, @@ -38,7 +37,8 @@ } }, "name": "name" - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json b/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json index a439b65dd0..ee19d58742 100644 --- a/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json +++ b/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json @@ -104,7 +104,8 @@ }, "value": "svelte", "raw": "'svelte'" - } + }, + "attributes": [] }, { "type": "ExpressionStatement", @@ -257,7 +258,8 @@ }, "value": "./foo.js", "raw": "'./foo.js'" - } + }, + "options": null }, "property": { "type": "Identifier", diff --git a/packages/svelte/tests/parser-legacy/samples/event-handler/output.json b/packages/svelte/tests/parser-legacy/samples/event-handler/output.json index 45b6256677..11ee562297 100644 --- a/packages/svelte/tests/parser-legacy/samples/event-handler/output.json +++ b/packages/svelte/tests/parser-legacy/samples/event-handler/output.json @@ -15,7 +15,6 @@ "end": 45, "type": "EventHandler", "name": "click", - "modifiers": [], "expression": { "type": "ArrowFunctionExpression", "start": 19, @@ -100,7 +99,8 @@ } } } - } + }, + "modifiers": [] } ], "children": [ diff --git a/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json b/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json index 15db05904c..42229b741f 100644 --- a/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json +++ b/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json @@ -22,7 +22,6 @@ "end": 692, "type": "EventHandler", "name": "click", - "modifiers": [], "expression": { "type": "ArrowFunctionExpression", "start": 596, @@ -137,7 +136,8 @@ "end": 594 } ] - } + }, + "modifiers": [] } ], "children": [ diff --git a/packages/svelte/tests/parser-legacy/samples/refs/output.json b/packages/svelte/tests/parser-legacy/samples/refs/output.json index e2bda741fa..7829a2787f 100644 --- a/packages/svelte/tests/parser-legacy/samples/refs/output.json +++ b/packages/svelte/tests/parser-legacy/samples/refs/output.json @@ -22,7 +22,6 @@ "end": 53, "type": "Binding", "name": "this", - "modifiers": [], "expression": { "type": "Identifier", "start": 49, @@ -38,7 +37,8 @@ } }, "name": "foo" - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/transition-intro-no-params/output.json b/packages/svelte/tests/parser-legacy/samples/transition-intro-no-params/output.json index f30788d758..18860d615b 100644 --- a/packages/svelte/tests/parser-legacy/samples/transition-intro-no-params/output.json +++ b/packages/svelte/tests/parser-legacy/samples/transition-intro-no-params/output.json @@ -15,8 +15,8 @@ "end": 12, "type": "Transition", "name": "fade", - "modifiers": [], "expression": null, + "modifiers": [], "intro": true, "outro": false } diff --git a/packages/svelte/tests/parser-legacy/samples/transition-intro/output.json b/packages/svelte/tests/parser-legacy/samples/transition-intro/output.json index ae52f72c5d..973cfb7d33 100644 --- a/packages/svelte/tests/parser-legacy/samples/transition-intro/output.json +++ b/packages/svelte/tests/parser-legacy/samples/transition-intro/output.json @@ -15,7 +15,6 @@ "end": 30, "type": "Transition", "name": "style", - "modifiers": [], "expression": { "type": "ObjectExpression", "start": 16, @@ -85,6 +84,7 @@ } ] }, + "modifiers": [], "intro": true, "outro": false } diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select/_config.js index 2507f5fc83..996f68e39f 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-select/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select/_config.js @@ -25,7 +25,7 @@ export default test({

selected: one

@@ -54,7 +54,7 @@ export default test({

selected: two

diff --git a/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js b/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js index fe6a29207d..1e95aaafa6 100644 --- a/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js @@ -4,7 +4,9 @@ export default test({ html: ` - + + ` }); diff --git a/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js b/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js index 3be9f0e925..b7ecd04def 100644 --- a/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js @@ -9,7 +9,7 @@ export default test({ - +
hi
`, diff --git a/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js b/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js index 4c94ea1e01..df03b7a053 100644 --- a/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js @@ -7,7 +7,7 @@ export default test({ target.innerHTML, ` selected: a @@ -23,7 +23,7 @@ export default test({ target.innerHTML, ` selected: b diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 690a7e3d98..c0d1177a82 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -7,7 +7,7 @@ import { flushSync, hydrate, mount, unmount } from 'svelte'; import { render } from 'svelte/server'; import { afterAll, assert, beforeAll } from 'vitest'; import { compile_directory, fragments } from '../helpers.js'; -import { setup_html_equal } from '../html_equal.js'; +import { assert_html_equal, assert_html_equal_with_options } from '../html_equal.js'; import { raf } from '../animation-helpers.js'; import type { CompileOptions } from '#compiler'; import { suite_with_variants, type BaseTest } from '../suite.js'; @@ -86,10 +86,6 @@ function unhandled_rejection_handler(err: Error) { const listeners = process.rawListeners('unhandledRejection'); -const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal({ - removeDataSvelte: true -}); - beforeAll(() => { // @ts-expect-error TODO huh? process.prependListener('unhandledRejection', unhandled_rejection_handler); diff --git a/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/Child.svelte b/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/Child.svelte new file mode 100644 index 0000000000..0996a02b60 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/Child.svelte @@ -0,0 +1,6 @@ + + +{foo} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/_config.js b/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/_config.js new file mode 100644 index 0000000000..345654535b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `bar` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/main.svelte b/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/main.svelte new file mode 100644 index 0000000000..518f733144 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/main.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/_config.js index b3edfefa20..4e771dfafa 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/_config.js @@ -5,9 +5,8 @@ export default test({ test({ assert, target }) { const btn = target.querySelector('button'); - btn?.click(); - flushSync(); - - assert.htmlEqual(target.innerHTML, `

Error occured

`); + assert.throws(() => { + flushSync(() => btn?.click()); + }, /kaboom/); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte index bae46c2590..d9dee1e2b0 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte @@ -1,20 +1,18 @@ - {#each things as thing} -

{thing}

- {/each} + {d} {#snippet failed()}

Error occured

diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/Child.svelte index 6e607871d3..de482da985 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/Child.svelte @@ -1,7 +1,14 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/_config.js index b3edfefa20..3825dc7811 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/_config.js @@ -8,6 +8,6 @@ export default test({ btn?.click(); flushSync(); - assert.htmlEqual(target.innerHTML, `

Error occured

`); + assert.htmlEqual(target.innerHTML, `

Error occurred

`); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/main.svelte index 65b71106fa..56e4846675 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/main.svelte @@ -2,21 +2,14 @@ import Child from './Child.svelte'; let count = $state(0); - - const things = $derived.by(() => { - if (count === 1) { - throw new Error('123') - } - return [1, 2 ,3] - }) - + {#snippet failed()} -

Error occured

+

Error occurred

{/snippet}
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js index fedaaa9ad1..e092d0e7c7 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js @@ -5,11 +5,11 @@ export default test({ test({ assert, target }) { let btn = target.querySelector('button'); - btn?.click(); - btn?.click(); - assert.throws(() => { - flushSync(); + flushSync(() => { + btn?.click(); + btn?.click(); + }); }, /test\n\n\tin {expression}\n/); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/main.svelte index b9de7126db..e73dcacc8a 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/main.svelte @@ -1,16 +1,18 @@ { throw(e) }}> -
Count: {count}
- - {count} / {test} +
Count: {count}
+ + {count} / {maybe_throw()}
diff --git a/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/_config.js b/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/_config.js new file mode 100644 index 0000000000..33a59dfbd3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/_config.js @@ -0,0 +1,16 @@ +import { ok, test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const btn = target.querySelector('button'); + const main = target.querySelector('main'); + ok(main); + console.log(main.innerHTML); + assert.htmlEqual(main.innerHTML, `
true
`); + // we don't want to use flush sync (or tick that use it inside) since we are testing that calling `flushSync` once + // when there are no scheduled effects does not cause reactivity to break + btn?.click(); + await Promise.resolve(); + assert.htmlEqual(main.innerHTML, `
false
false
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/main.svelte b/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/main.svelte new file mode 100644 index 0000000000..448d495cf5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/main.svelte @@ -0,0 +1,23 @@ + + + + +
+
{flag}
+ + {#if !flag} +
{test}
+ {/if} +
+ diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-destructure-array/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-destructure-array/_config.js new file mode 100644 index 0000000000..05e3e80b79 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-destructure-array/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `a` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-destructure-array/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-destructure-array/main.svelte new file mode 100644 index 0000000000..37345c629e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-destructure-array/main.svelte @@ -0,0 +1,9 @@ + + +{#snippet content([x])} + {x} +{/snippet} + +{@render content(array)} \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/destructure-state-iterable/_expected.html b/packages/svelte/tests/server-side-rendering/samples/destructure-state-iterable/_expected.html new file mode 100644 index 0000000000..e3f755a08c --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/destructure-state-iterable/_expected.html @@ -0,0 +1 @@ +0, 1 \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/destructure-state-iterable/main.svelte b/packages/svelte/tests/server-side-rendering/samples/destructure-state-iterable/main.svelte new file mode 100644 index 0000000000..9414735f2f --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/destructure-state-iterable/main.svelte @@ -0,0 +1,11 @@ + + +{one}, {two} \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/destructure-state/_expected.html b/packages/svelte/tests/server-side-rendering/samples/destructure-state/_expected.html new file mode 100644 index 0000000000..213a5f5bf1 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/destructure-state/_expected.html @@ -0,0 +1 @@ +10, Admin \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/destructure-state/main.svelte b/packages/svelte/tests/server-side-rendering/samples/destructure-state/main.svelte new file mode 100644 index 0000000000..422548cd14 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/destructure-state/main.svelte @@ -0,0 +1,5 @@ + + +{level}, {custom} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d5db0fd8e..cfbc54df33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,8 +87,8 @@ importers: specifier: ^1.2.1 version: 1.2.1 esrap: - specifier: ^1.4.6 - version: 1.4.6 + specifier: ^1.4.8 + version: 1.4.8 is-reference: specifier: ^3.0.3 version: 3.0.3 @@ -1261,8 +1261,8 @@ packages: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} - esrap@1.4.6: - resolution: {integrity: sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==} + esrap@1.4.8: + resolution: {integrity: sha512-jlENbjZ7lqgJV9/OmgAtVqrFFMwsl70ctOgPIg5oTdQVGC13RSkMdtvAmu7ZTLax92c9ljnIG0xleEkdL69hwg==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -3622,7 +3622,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@1.4.6: + esrap@1.4.8: dependencies: '@jridgewell/sourcemap-codec': 1.5.0