From 007d5f363f44585020381ba6293b22cf88bac646 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 5 Nov 2024 17:46:23 +0000 Subject: [PATCH 01/43] feat: add error boundary support tweak tweak again retry -> reset tweaks add tests tweaks tweaks tweaks more tests more tests and tweaks comments tweak tweak tweak tweak tweak --- packages/svelte/elements.d.ts | 4 + .../compiler/phases/1-parse/state/element.js | 3 +- .../3-transform/client/transform-client.js | 2 + .../client/visitors/SvelteBoundary.js | 108 ++++++++++++++++ .../3-transform/server/transform-server.js | 4 +- .../server/visitors/SvelteBoundary.js | 20 +++ .../src/compiler/phases/3-transform/utils.js | 1 + .../svelte/src/compiler/types/template.d.ts | 8 +- .../svelte/src/internal/client/constants.js | 27 ++-- .../internal/client/dom/blocks/boundary.js | 121 ++++++++++++++++++ .../src/internal/client/dom/template.js | 1 - packages/svelte/src/internal/client/index.js | 1 + .../svelte/src/internal/client/runtime.js | 48 ++++++- .../samples/svelte-selfdestructive/_config.js | 2 +- .../samples/error-boundary-10/_config.js | 14 ++ .../samples/error-boundary-10/main.svelte | 17 +++ .../samples/error-boundary-11/_config.js | 14 ++ .../samples/error-boundary-11/main.svelte | 17 +++ .../samples/error-boundary-2/_config.js | 13 ++ .../samples/error-boundary-2/main.svelte | 15 +++ .../samples/error-boundary-3/_config.js | 14 ++ .../samples/error-boundary-3/main.svelte | 17 +++ .../samples/error-boundary-4/_config.js | 14 ++ .../samples/error-boundary-4/main.svelte | 17 +++ .../samples/error-boundary-5/Child.svelte | 13 ++ .../samples/error-boundary-5/_config.js | 31 +++++ .../samples/error-boundary-5/main.svelte | 18 +++ .../samples/error-boundary-6/_config.js | 13 ++ .../samples/error-boundary-6/main.svelte | 20 +++ .../samples/error-boundary-7/Child.svelte | 5 + .../samples/error-boundary-7/_config.js | 8 ++ .../samples/error-boundary-7/main.svelte | 14 ++ .../samples/error-boundary-8/Child.svelte | 5 + .../samples/error-boundary-8/_config.js | 8 ++ .../samples/error-boundary-8/main.svelte | 17 +++ .../samples/error-boundary-9/Child.svelte | 5 + .../samples/error-boundary-9/_config.js | 8 ++ .../samples/error-boundary-9/main.svelte | 20 +++ .../samples/error-boundary/_config.js | 9 ++ .../samples/error-boundary/main.svelte | 11 ++ packages/svelte/types/index.d.ts | 8 +- 41 files changed, 690 insertions(+), 25 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js create mode 100644 packages/svelte/src/internal/client/dom/blocks/boundary.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-10/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-10/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-11/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-11/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-2/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-4/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-4/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-5/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-5/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-5/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-6/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-6/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-7/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-7/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-7/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-8/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-8/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-8/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-9/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-9/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-9/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary/main.svelte diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 8746b29e250a..518b4328dcc8 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -2026,6 +2026,10 @@ export interface SvelteHTMLElements { [name: string]: any; }; 'svelte:head': { [name: string]: any }; + 'svelte:boundary': { + onerror?: (error: Error, reset: () => void) => void; + failed?: import('svelte').Snippet; + }; [name: string]: { [name: string]: any }; } diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 9082b76c4972..68a800636064 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -43,7 +43,8 @@ const meta_tags = new Map([ ['svelte:element', 'SvelteElement'], ['svelte:component', 'SvelteComponent'], ['svelte:self', 'SvelteSelf'], - ['svelte:fragment', 'SvelteFragment'] + ['svelte:fragment', 'SvelteFragment'], + ['svelte:boundary', 'SvelteBoundary'] ]); /** @param {Parser} parser */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 5349f6025533..99cb78358a8e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -48,6 +48,7 @@ import { SvelteComponent } from './visitors/SvelteComponent.js'; import { SvelteDocument } from './visitors/SvelteDocument.js'; import { SvelteElement } from './visitors/SvelteElement.js'; import { SvelteFragment } from './visitors/SvelteFragment.js'; +import { SvelteBoundary } from './visitors/SvelteBoundary.js'; import { SvelteHead } from './visitors/SvelteHead.js'; import { SvelteSelf } from './visitors/SvelteSelf.js'; import { SvelteWindow } from './visitors/SvelteWindow.js'; @@ -122,6 +123,7 @@ const visitors = { SvelteDocument, SvelteElement, SvelteFragment, + SvelteBoundary, SvelteHead, SvelteSelf, SvelteWindow, 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 new file mode 100644 index 000000000000..97a34a8fc5b2 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -0,0 +1,108 @@ +/** @import { BlockStatement, Statement, Property, Expression } from 'estree' */ +/** @import { AST } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ + +import * as b from '../../../../utils/builders.js'; +/** + * @param {AST.SvelteBoundary} node + * @param {ComponentContext} context + */ +export function SvelteBoundary(node, context) { + const nodes = []; + /** @type {Statement[]} */ + const snippet_statements = []; + /** @type {Array} */ + const props_and_spreads = []; + + let has_spread = false; + + const push_prop = (/** @type {Property} */ prop) => { + let current = props_and_spreads.at(-1); + if (Array.isArray(current)) { + current.push(prop); + } + const arr = [prop]; + props_and_spreads.push(arr); + }; + + for (const attribute of node.attributes) { + if (attribute.type === 'SpreadAttribute') { + const value = /** @type {Expression} */ (context.visit(attribute.expression, context.state)); + has_spread = true; + + if (attribute.metadata.expression.has_state) { + props_and_spreads.push(b.thunk(value)); + } else { + props_and_spreads.push(value); + } + continue; + } + + // Skip non-attributes with a single value + if ( + attribute.type !== 'Attribute' || + attribute.value === true || + Array.isArray(attribute.value) + ) { + continue; + } + + // Currently we only support `onerror` and `failed` props + if (attribute.name === 'onerror' || attribute.name === 'failed') { + const value = /** @type {Expression} */ ( + context.visit(attribute.value.expression, context.state) + ); + + if (attribute.metadata.expression.has_state) { + push_prop( + b.prop('get', b.id(attribute.name), b.function(null, [], b.block([b.return(value)]))) + ); + } else { + push_prop(b.prop('init', b.id(attribute.name), value)); + } + } + } + + // Capture the `failed` implicit snippet prop + for (const child of node.fragment.nodes) { + if (child.type === 'SnippetBlock' && child.expression.name === 'failed') { + /** @type {Statement[]} */ + const init = []; + const block_state = { ...context.state, init }; + context.visit(child, block_state); + push_prop(b.prop('init', b.id('failed'), b.id('failed'))); + snippet_statements.push(...init); + } else { + nodes.push(child); + } + } + + const block = /** @type {BlockStatement} */ ( + context.visit( + { + ...node.fragment, + nodes + }, + { ...context.state } + ) + ); + + const props_expression = + !has_spread && Array.isArray(props_and_spreads[0]) + ? b.object(props_and_spreads[0]) + : props_and_spreads.length === 0 + ? b.object([]) + : b.call( + '$.spread_props', + ...props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p)) + ); + + const boundary = b.stmt( + b.call('$.boundary', context.state.node, b.arrow([b.id('$$anchor')], block), props_expression) + ); + + context.state.template.push(''); + context.state.init.push( + snippet_statements ? b.block([...snippet_statements, boundary]) : boundary + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index c164aa421916..ffde76fab7af 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -38,6 +38,7 @@ import { SvelteSelf } from './visitors/SvelteSelf.js'; import { TitleElement } from './visitors/TitleElement.js'; import { UpdateExpression } from './visitors/UpdateExpression.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js'; +import { SvelteBoundary } from './visitors/SvelteBoundary.js'; /** @type {Visitors} */ const global_visitors = { @@ -75,7 +76,8 @@ const template_visitors = { SvelteFragment, SvelteHead, SvelteSelf, - TitleElement + TitleElement, + SvelteBoundary }; /** diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js new file mode 100644 index 000000000000..b8797f3f501c --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js @@ -0,0 +1,20 @@ +/** @import { BlockStatement } from 'estree' */ +/** @import { AST } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ + +import { + BLOCK_CLOSE, + BLOCK_OPEN, + EMPTY_COMMENT +} from '../../../../../internal/server/hydration.js'; +import * as b from '../../../../utils/builders.js'; + +/** + * @param {AST.SvelteBoundary} node + * @param {ComponentContext} context + */ +export function SvelteBoundary(node, context) { + context.state.template.push(b.literal(BLOCK_OPEN)); + context.state.template.push(/** @type {BlockStatement} */ (context.visit(node.fragment))); + context.state.template.push(b.literal(BLOCK_CLOSE)); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 809c627098e8..4496269889b5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -306,6 +306,7 @@ export function clean_nodes( parent.type === 'SnippetBlock' || parent.type === 'EachBlock' || parent.type === 'SvelteComponent' || + parent.type === 'SvelteBoundary' || parent.type === 'Component' || parent.type === 'SvelteSelf') && first && diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index fd1824d3b3db..78a637cfa8fd 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -352,6 +352,11 @@ export namespace AST { name: 'svelte:fragment'; } + export interface SvelteBoundary extends BaseElement { + type: 'SvelteBoundary'; + name: 'svelte:boundary'; + } + export interface SvelteHead extends BaseElement { type: 'SvelteHead'; name: 'svelte:head'; @@ -499,7 +504,8 @@ export type ElementLike = | AST.SvelteHead | AST.SvelteOptionsRaw | AST.SvelteSelf - | AST.SvelteWindow; + | AST.SvelteWindow + | AST.SvelteBoundary; export type TemplateNode = | AST.Root diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 53df86126ac4..050d287f2a62 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -4,21 +4,22 @@ export const RENDER_EFFECT = 1 << 3; export const BLOCK_EFFECT = 1 << 4; export const BRANCH_EFFECT = 1 << 5; export const ROOT_EFFECT = 1 << 6; -export const UNOWNED = 1 << 7; -export const DISCONNECTED = 1 << 8; -export const CLEAN = 1 << 9; -export const DIRTY = 1 << 10; -export const MAYBE_DIRTY = 1 << 11; -export const INERT = 1 << 12; -export const DESTROYED = 1 << 13; -export const EFFECT_RAN = 1 << 14; +export const BOUNDARY_EFFECT = 1 << 7; +export const UNOWNED = 1 << 8; +export const DISCONNECTED = 1 << 9; +export const CLEAN = 1 << 10; +export const DIRTY = 1 << 11; +export const MAYBE_DIRTY = 1 << 12; +export const INERT = 1 << 13; +export const DESTROYED = 1 << 14; +export const EFFECT_RAN = 1 << 15; /** 'Transparent' effects do not create a transition boundary */ -export const EFFECT_TRANSPARENT = 1 << 15; +export const EFFECT_TRANSPARENT = 1 << 16; /** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */ -export const LEGACY_DERIVED_PROP = 1 << 16; -export const INSPECT_EFFECT = 1 << 17; -export const HEAD_EFFECT = 1 << 18; -export const EFFECT_HAS_DERIVED = 1 << 19; +export const LEGACY_DERIVED_PROP = 1 << 17; +export const INSPECT_EFFECT = 1 << 18; +export const HEAD_EFFECT = 1 << 19; +export const EFFECT_HAS_DERIVED = 1 << 20; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js new file mode 100644 index 000000000000..8fbce24291bb --- /dev/null +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -0,0 +1,121 @@ +/** @import { Effect, TemplateNode, } from '#client' */ + +import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js'; +import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; +import { + active_effect, + active_reaction, + component_context, + set_active_effect, + set_active_reaction, + set_component_context +} from '../../runtime.js'; +import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; +import { queue_micro_task } from '../task.js'; + +/** + * @param {Effect} boundary + * @param {() => void} fn + */ +function with_boundary(boundary, fn) { + var previous_effect = active_effect; + var previous_reaction = active_reaction; + var previous_ctx = component_context; + set_active_effect(boundary); + set_active_reaction(boundary); + set_component_context(boundary.ctx); + try { + fn(); + } finally { + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_ctx); + } +} + +/** + * @param {TemplateNode} node + * @param {((anchor: Node) => void)} boundary_fn + * @param {{ + * onerror?: (error: Error, reset: () => void) => void, + * failed?: (anchor: Node, error: () => Error, reset: () => () => void) => void + * }} props + * @returns {void} + */ +export function boundary(node, boundary_fn, props) { + var anchor = node; + + /** @type {Effect | null} */ + var boundary_effect; + + block(() => { + var boundary = /** @type {Effect} */ (active_effect); + var start = hydrate_node; + + // We re-use the effect's fn property to avoid allocation of an additional field + boundary.fn = (/** @type {{ error?: Error }} */ payload) => { + let { error } = payload; + + // In the future, boundaries might handle other things other than errors + if (!error) { + return; + } + + var onerror = props.onerror; + let failed_snippet = props.failed; + + if (boundary_effect) { + destroy_effect(boundary_effect); + } + + // If we have nothing to capture the error then rethrow the error + // for another boundary to handle + if (!onerror && !failed_snippet) { + throw error; + } + + // Handle resetting the error boundary + var reset = () => { + if (boundary_effect) { + pause_effect(boundary_effect); + } + with_boundary(boundary, () => { + boundary_effect = null; + boundary_effect = branch(() => boundary_fn(anchor)); + }); + }; + + // Handle the `onerror` event handler + if (onerror) { + onerror(error, reset); + } + + // Handle the `failed` snippet fallback + if (failed_snippet) { + // Ensure we create the boundary branch after the catch event cycle finishes + queue_micro_task(() => { + with_boundary(boundary, () => { + boundary_effect = null; + boundary_effect = branch(() => + failed_snippet( + anchor, + () => error, + () => reset + ) + ); + }); + }); + } + }; + + if (hydrating) { + hydrate_next(); + } + + boundary_effect = branch(() => boundary_fn(anchor)); + }, EFFECT_TRANSPARENT | BOUNDARY_EFFECT); + + if (hydrating) { + anchor = hydrate_node; + } +} diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 7ccffc7d2c80..bcbae393ecff 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -4,7 +4,6 @@ import { create_text, get_first_child } from './operations.js'; import { create_fragment_from_html } from './reconciler.js'; import { active_effect } from '../runtime.js'; import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; -import { queue_micro_task } from './task.js'; /** * @param {TemplateNode} start diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c401867a0f0b..466c71e5c6eb 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -124,6 +124,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; +export { boundary } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get, diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index e9f1c773e689..7e5c73728bd2 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -24,7 +24,8 @@ import { BLOCK_EFFECT, ROOT_EFFECT, LEGACY_DERIVED_PROP, - DISCONNECTED + DISCONNECTED, + BOUNDARY_EFFECT } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { add_owner } from './dev/ownership.js'; @@ -228,15 +229,51 @@ export function check_dirtiness(reaction) { return false; } +/** + * @param {Error} error + * @param {Effect} effect + */ +function propagate_error(error, effect) { + /** @type {Effect | null} */ + var current = effect; + while (current !== null) { + /** @type {Effect | null} */ + var parent = current.parent; + + if ((current.f & BOUNDARY_EFFECT) !== 0) { + try { + // @ts-ignore + current.fn({ error }); + } catch { + current = parent; + continue; + } + return; + } + current = parent; + } + + throw error; +} + /** * @param {Error} error * @param {Effect} effect * @param {ComponentContext | null} component_context */ function handle_error(error, effect, component_context) { - // Given we don't yet have error boundaries, we will just always throw. - if (!DEV || handled_errors.has(error) || component_context === null) { - throw error; + if (handled_errors.has(error)) { + // If the parent is not an error boundary then re-throw the error + if (effect.parent === null || (effect.parent.f & BOUNDARY_EFFECT) === 0) { + throw error; + } + return; + } + handled_errors.add(error); + + if (!DEV || component_context === null) { + propagate_error(error, effect); + return; } const component_stack = []; @@ -287,8 +324,7 @@ function handle_error(error, effect, component_context) { }); } - handled_errors.add(error); - throw error; + propagate_error(error, effect); } /** diff --git a/packages/svelte/tests/compiler-errors/samples/svelte-selfdestructive/_config.js b/packages/svelte/tests/compiler-errors/samples/svelte-selfdestructive/_config.js index 371ddb81db50..de5ee3bb6909 100644 --- a/packages/svelte/tests/compiler-errors/samples/svelte-selfdestructive/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/svelte-selfdestructive/_config.js @@ -4,7 +4,7 @@ export default test({ error: { code: 'svelte_meta_invalid_tag', message: - 'Valid `` tag names are svelte:head, svelte:options, svelte:window, svelte:document, svelte:body, svelte:element, svelte:component, svelte:self or svelte:fragment', + 'Valid `` tag names are svelte:head, svelte:options, svelte:window, svelte:document, svelte:body, svelte:element, svelte:component, svelte:self, svelte:fragment or svelte:boundary', position: [10, 32] } }); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-10/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-10/_config.js new file mode 100644 index 000000000000..b8858d41fd90 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-10/_config.js @@ -0,0 +1,14 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + const [btn1, btn2] = target.querySelectorAll('button'); + + btn2?.click(); + btn1?.click(); + flushSync(); + + assert.deepEqual(logs, ['error caught!!!']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-10/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-10/main.svelte new file mode 100644 index 000000000000..06c5e8004bc1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-10/main.svelte @@ -0,0 +1,17 @@ + + + + {count > 0 ? throw_error() : null} + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-11/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-11/_config.js new file mode 100644 index 000000000000..b8858d41fd90 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-11/_config.js @@ -0,0 +1,14 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + const [btn1, btn2] = target.querySelectorAll('button'); + + btn2?.click(); + btn1?.click(); + flushSync(); + + assert.deepEqual(logs, ['error caught!!!']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-11/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-11/main.svelte new file mode 100644 index 000000000000..015c5e2abba9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-11/main.svelte @@ -0,0 +1,17 @@ + + + + {count > 0 ? throw_error() : null} + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-2/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-2/_config.js new file mode 100644 index 000000000000..5e7803fc30f1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-2/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + + assert.deepEqual(logs, ['error caught']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-2/main.svelte new file mode 100644 index 000000000000..0f1c7fb54d82 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-2/main.svelte @@ -0,0 +1,15 @@ + + + console.log('error caught')}> + {count > 0 ? throw_error() : null} + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js new file mode 100644 index 000000000000..040e13676eed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js @@ -0,0 +1,14 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + + assert.deepEqual(logs, ['error caught']); + assert.htmlEqual(target.innerHTML, `
Fallback!
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte new file mode 100644 index 000000000000..bad84666c018 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte @@ -0,0 +1,17 @@ + + + console.log('error caught')}> + {count > 0 ? throw_error() : null} + + {#snippet failed()} +
Fallback!
+ {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-4/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-4/_config.js new file mode 100644 index 000000000000..040e13676eed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-4/_config.js @@ -0,0 +1,14 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + + assert.deepEqual(logs, ['error caught']); + assert.htmlEqual(target.innerHTML, `
Fallback!
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-4/main.svelte new file mode 100644 index 000000000000..36cb2b1b95a1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-4/main.svelte @@ -0,0 +1,17 @@ + + +{#snippet failed()} +
Fallback!
+{/snippet} + + console.log('error caught')}> + {count > 0 ? throw_error() : null} + + + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-5/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-5/Child.svelte new file mode 100644 index 000000000000..0b78b5073660 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-5/Child.svelte @@ -0,0 +1,13 @@ + + +{count} + + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-5/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-5/_config.js new file mode 100644 index 000000000000..791a4ec7a934 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-5/_config.js @@ -0,0 +1,31 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + let btn = target.querySelector('button'); + + btn?.click(); + btn?.click(); + flushSync(); + + assert.deepEqual(logs, ['error caught']); + assert.htmlEqual(target.innerHTML, `
too high
`); + + const [btn2] = target.querySelectorAll('button'); + + btn2?.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `0\n`); + + btn = target.querySelector('button'); + + btn?.click(); + btn?.click(); + flushSync(); + + assert.deepEqual(logs, ['error caught', 'error caught']); + assert.htmlEqual(target.innerHTML, `
too high
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-5/main.svelte new file mode 100644 index 000000000000..f32a43b2cc2a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-5/main.svelte @@ -0,0 +1,18 @@ + + + console.log('error caught')}> + { throw e }}> + + + + + + {#snippet failed(e, retry)} +
{e.message}
+ + {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-6/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-6/_config.js new file mode 100644 index 000000000000..6a895cfa3618 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-6/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `
There is an error!
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-6/main.svelte new file mode 100644 index 000000000000..0ea6d964e885 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-6/main.svelte @@ -0,0 +1,20 @@ + + + error = e}> + {count > 0 ? throw_error() : null} + + + + +{#if error} +
There is an error!
+{/if} + + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-7/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-7/Child.svelte new file mode 100644 index 000000000000..35e4424414d7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-7/Child.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-7/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-7/_config.js new file mode 100644 index 000000000000..9664c233b7d5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-7/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + assert.deepEqual(logs, ['error caught']); + assert.htmlEqual(target.innerHTML, `
Error!
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-7/main.svelte new file mode 100644 index 000000000000..e94dc34b48ce --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-7/main.svelte @@ -0,0 +1,14 @@ + + + console.log('error caught')}> + + + {#snippet failed(e, retry)} +
Error!
+ + {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-8/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-8/Child.svelte new file mode 100644 index 000000000000..35e4424414d7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-8/Child.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-8/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-8/_config.js new file mode 100644 index 000000000000..9664c233b7d5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-8/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + assert.deepEqual(logs, ['error caught']); + assert.htmlEqual(target.innerHTML, `
Error!
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-8/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-8/main.svelte new file mode 100644 index 000000000000..2cd07dbb46ac --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-8/main.svelte @@ -0,0 +1,17 @@ + + + console.log('error caught')}> + + + + + + {#snippet failed(e, retry)} +
Error!
+ + {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-9/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-9/Child.svelte new file mode 100644 index 000000000000..35e4424414d7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-9/Child.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-9/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-9/_config.js new file mode 100644 index 000000000000..9664c233b7d5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-9/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + assert.deepEqual(logs, ['error caught']); + assert.htmlEqual(target.innerHTML, `
Error!
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-9/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-9/main.svelte new file mode 100644 index 000000000000..082fe873eeda --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-9/main.svelte @@ -0,0 +1,20 @@ + + + console.log('error caught')}> + + + + { throw e }}> + + + + + {#snippet failed(e, retry)} +
Error!
+ + {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary/_config.js new file mode 100644 index 000000000000..1befa47b2742 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + test({ assert, logs }) { + assert.deepEqual(logs, ['error caught']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary/main.svelte new file mode 100644 index 000000000000..36bf871b5ae5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary/main.svelte @@ -0,0 +1,11 @@ + + + console.log('error caught')}> + {throw_error()} + + + diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 00ba2556d9a1..2eeb55eeb88a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1163,6 +1163,11 @@ declare module 'svelte/compiler' { name: 'svelte:fragment'; } + export interface SvelteBoundary extends BaseElement { + type: 'SvelteBoundary'; + name: 'svelte:boundary'; + } + export interface SvelteHead extends BaseElement { type: 'SvelteHead'; name: 'svelte:head'; @@ -1280,7 +1285,8 @@ declare module 'svelte/compiler' { | AST.SvelteHead | AST.SvelteOptionsRaw | AST.SvelteSelf - | AST.SvelteWindow; + | AST.SvelteWindow + | AST.SvelteBoundary; /** * The preprocess function provides convenient hooks for arbitrarily transforming component source code. * For example, it can be used to convert a `