Skip to content

Commit 007d5f3

Browse files
committed
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
1 parent d7caf08 commit 007d5f3

File tree

41 files changed

+690
-25
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+690
-25
lines changed

packages/svelte/elements.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2026,6 +2026,10 @@ export interface SvelteHTMLElements {
20262026
[name: string]: any;
20272027
};
20282028
'svelte:head': { [name: string]: any };
2029+
'svelte:boundary': {
2030+
onerror?: (error: Error, reset: () => void) => void;
2031+
failed?: import('svelte').Snippet;
2032+
};
20292033

20302034
[name: string]: { [name: string]: any };
20312035
}

packages/svelte/src/compiler/phases/1-parse/state/element.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ const meta_tags = new Map([
4343
['svelte:element', 'SvelteElement'],
4444
['svelte:component', 'SvelteComponent'],
4545
['svelte:self', 'SvelteSelf'],
46-
['svelte:fragment', 'SvelteFragment']
46+
['svelte:fragment', 'SvelteFragment'],
47+
['svelte:boundary', 'SvelteBoundary']
4748
]);
4849

4950
/** @param {Parser} parser */

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { SvelteComponent } from './visitors/SvelteComponent.js';
4848
import { SvelteDocument } from './visitors/SvelteDocument.js';
4949
import { SvelteElement } from './visitors/SvelteElement.js';
5050
import { SvelteFragment } from './visitors/SvelteFragment.js';
51+
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
5152
import { SvelteHead } from './visitors/SvelteHead.js';
5253
import { SvelteSelf } from './visitors/SvelteSelf.js';
5354
import { SvelteWindow } from './visitors/SvelteWindow.js';
@@ -122,6 +123,7 @@ const visitors = {
122123
SvelteDocument,
123124
SvelteElement,
124125
SvelteFragment,
126+
SvelteBoundary,
125127
SvelteHead,
126128
SvelteSelf,
127129
SvelteWindow,
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/** @import { BlockStatement, Statement, Property, Expression } from 'estree' */
2+
/** @import { AST } from '#compiler' */
3+
/** @import { ComponentContext } from '../types' */
4+
5+
import * as b from '../../../../utils/builders.js';
6+
/**
7+
* @param {AST.SvelteBoundary} node
8+
* @param {ComponentContext} context
9+
*/
10+
export function SvelteBoundary(node, context) {
11+
const nodes = [];
12+
/** @type {Statement[]} */
13+
const snippet_statements = [];
14+
/** @type {Array<Property[] | Expression>} */
15+
const props_and_spreads = [];
16+
17+
let has_spread = false;
18+
19+
const push_prop = (/** @type {Property} */ prop) => {
20+
let current = props_and_spreads.at(-1);
21+
if (Array.isArray(current)) {
22+
current.push(prop);
23+
}
24+
const arr = [prop];
25+
props_and_spreads.push(arr);
26+
};
27+
28+
for (const attribute of node.attributes) {
29+
if (attribute.type === 'SpreadAttribute') {
30+
const value = /** @type {Expression} */ (context.visit(attribute.expression, context.state));
31+
has_spread = true;
32+
33+
if (attribute.metadata.expression.has_state) {
34+
props_and_spreads.push(b.thunk(value));
35+
} else {
36+
props_and_spreads.push(value);
37+
}
38+
continue;
39+
}
40+
41+
// Skip non-attributes with a single value
42+
if (
43+
attribute.type !== 'Attribute' ||
44+
attribute.value === true ||
45+
Array.isArray(attribute.value)
46+
) {
47+
continue;
48+
}
49+
50+
// Currently we only support `onerror` and `failed` props
51+
if (attribute.name === 'onerror' || attribute.name === 'failed') {
52+
const value = /** @type {Expression} */ (
53+
context.visit(attribute.value.expression, context.state)
54+
);
55+
56+
if (attribute.metadata.expression.has_state) {
57+
push_prop(
58+
b.prop('get', b.id(attribute.name), b.function(null, [], b.block([b.return(value)])))
59+
);
60+
} else {
61+
push_prop(b.prop('init', b.id(attribute.name), value));
62+
}
63+
}
64+
}
65+
66+
// Capture the `failed` implicit snippet prop
67+
for (const child of node.fragment.nodes) {
68+
if (child.type === 'SnippetBlock' && child.expression.name === 'failed') {
69+
/** @type {Statement[]} */
70+
const init = [];
71+
const block_state = { ...context.state, init };
72+
context.visit(child, block_state);
73+
push_prop(b.prop('init', b.id('failed'), b.id('failed')));
74+
snippet_statements.push(...init);
75+
} else {
76+
nodes.push(child);
77+
}
78+
}
79+
80+
const block = /** @type {BlockStatement} */ (
81+
context.visit(
82+
{
83+
...node.fragment,
84+
nodes
85+
},
86+
{ ...context.state }
87+
)
88+
);
89+
90+
const props_expression =
91+
!has_spread && Array.isArray(props_and_spreads[0])
92+
? b.object(props_and_spreads[0])
93+
: props_and_spreads.length === 0
94+
? b.object([])
95+
: b.call(
96+
'$.spread_props',
97+
...props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p))
98+
);
99+
100+
const boundary = b.stmt(
101+
b.call('$.boundary', context.state.node, b.arrow([b.id('$$anchor')], block), props_expression)
102+
);
103+
104+
context.state.template.push('<!>');
105+
context.state.init.push(
106+
snippet_statements ? b.block([...snippet_statements, boundary]) : boundary
107+
);
108+
}

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { SvelteSelf } from './visitors/SvelteSelf.js';
3838
import { TitleElement } from './visitors/TitleElement.js';
3939
import { UpdateExpression } from './visitors/UpdateExpression.js';
4040
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
41+
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
4142

4243
/** @type {Visitors} */
4344
const global_visitors = {
@@ -75,7 +76,8 @@ const template_visitors = {
7576
SvelteFragment,
7677
SvelteHead,
7778
SvelteSelf,
78-
TitleElement
79+
TitleElement,
80+
SvelteBoundary
7981
};
8082

8183
/**
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/** @import { BlockStatement } from 'estree' */
2+
/** @import { AST } from '#compiler' */
3+
/** @import { ComponentContext } from '../types' */
4+
5+
import {
6+
BLOCK_CLOSE,
7+
BLOCK_OPEN,
8+
EMPTY_COMMENT
9+
} from '../../../../../internal/server/hydration.js';
10+
import * as b from '../../../../utils/builders.js';
11+
12+
/**
13+
* @param {AST.SvelteBoundary} node
14+
* @param {ComponentContext} context
15+
*/
16+
export function SvelteBoundary(node, context) {
17+
context.state.template.push(b.literal(BLOCK_OPEN));
18+
context.state.template.push(/** @type {BlockStatement} */ (context.visit(node.fragment)));
19+
context.state.template.push(b.literal(BLOCK_CLOSE));
20+
}

packages/svelte/src/compiler/phases/3-transform/utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ export function clean_nodes(
306306
parent.type === 'SnippetBlock' ||
307307
parent.type === 'EachBlock' ||
308308
parent.type === 'SvelteComponent' ||
309+
parent.type === 'SvelteBoundary' ||
309310
parent.type === 'Component' ||
310311
parent.type === 'SvelteSelf') &&
311312
first &&

packages/svelte/src/compiler/types/template.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,11 @@ export namespace AST {
352352
name: 'svelte:fragment';
353353
}
354354

355+
export interface SvelteBoundary extends BaseElement {
356+
type: 'SvelteBoundary';
357+
name: 'svelte:boundary';
358+
}
359+
355360
export interface SvelteHead extends BaseElement {
356361
type: 'SvelteHead';
357362
name: 'svelte:head';
@@ -499,7 +504,8 @@ export type ElementLike =
499504
| AST.SvelteHead
500505
| AST.SvelteOptionsRaw
501506
| AST.SvelteSelf
502-
| AST.SvelteWindow;
507+
| AST.SvelteWindow
508+
| AST.SvelteBoundary;
503509

504510
export type TemplateNode =
505511
| AST.Root

packages/svelte/src/internal/client/constants.js

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,22 @@ export const RENDER_EFFECT = 1 << 3;
44
export const BLOCK_EFFECT = 1 << 4;
55
export const BRANCH_EFFECT = 1 << 5;
66
export const ROOT_EFFECT = 1 << 6;
7-
export const UNOWNED = 1 << 7;
8-
export const DISCONNECTED = 1 << 8;
9-
export const CLEAN = 1 << 9;
10-
export const DIRTY = 1 << 10;
11-
export const MAYBE_DIRTY = 1 << 11;
12-
export const INERT = 1 << 12;
13-
export const DESTROYED = 1 << 13;
14-
export const EFFECT_RAN = 1 << 14;
7+
export const BOUNDARY_EFFECT = 1 << 7;
8+
export const UNOWNED = 1 << 8;
9+
export const DISCONNECTED = 1 << 9;
10+
export const CLEAN = 1 << 10;
11+
export const DIRTY = 1 << 11;
12+
export const MAYBE_DIRTY = 1 << 12;
13+
export const INERT = 1 << 13;
14+
export const DESTROYED = 1 << 14;
15+
export const EFFECT_RAN = 1 << 15;
1516
/** 'Transparent' effects do not create a transition boundary */
16-
export const EFFECT_TRANSPARENT = 1 << 15;
17+
export const EFFECT_TRANSPARENT = 1 << 16;
1718
/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
18-
export const LEGACY_DERIVED_PROP = 1 << 16;
19-
export const INSPECT_EFFECT = 1 << 17;
20-
export const HEAD_EFFECT = 1 << 18;
21-
export const EFFECT_HAS_DERIVED = 1 << 19;
19+
export const LEGACY_DERIVED_PROP = 1 << 17;
20+
export const INSPECT_EFFECT = 1 << 18;
21+
export const HEAD_EFFECT = 1 << 19;
22+
export const EFFECT_HAS_DERIVED = 1 << 20;
2223

2324
export const STATE_SYMBOL = Symbol('$state');
2425
export const STATE_SYMBOL_METADATA = Symbol('$state metadata');
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/** @import { Effect, TemplateNode, } from '#client' */
2+
3+
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js';
4+
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
5+
import {
6+
active_effect,
7+
active_reaction,
8+
component_context,
9+
set_active_effect,
10+
set_active_reaction,
11+
set_component_context
12+
} from '../../runtime.js';
13+
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
14+
import { queue_micro_task } from '../task.js';
15+
16+
/**
17+
* @param {Effect} boundary
18+
* @param {() => void} fn
19+
*/
20+
function with_boundary(boundary, fn) {
21+
var previous_effect = active_effect;
22+
var previous_reaction = active_reaction;
23+
var previous_ctx = component_context;
24+
set_active_effect(boundary);
25+
set_active_reaction(boundary);
26+
set_component_context(boundary.ctx);
27+
try {
28+
fn();
29+
} finally {
30+
set_active_effect(previous_effect);
31+
set_active_reaction(previous_reaction);
32+
set_component_context(previous_ctx);
33+
}
34+
}
35+
36+
/**
37+
* @param {TemplateNode} node
38+
* @param {((anchor: Node) => void)} boundary_fn
39+
* @param {{
40+
* onerror?: (error: Error, reset: () => void) => void,
41+
* failed?: (anchor: Node, error: () => Error, reset: () => () => void) => void
42+
* }} props
43+
* @returns {void}
44+
*/
45+
export function boundary(node, boundary_fn, props) {
46+
var anchor = node;
47+
48+
/** @type {Effect | null} */
49+
var boundary_effect;
50+
51+
block(() => {
52+
var boundary = /** @type {Effect} */ (active_effect);
53+
var start = hydrate_node;
54+
55+
// We re-use the effect's fn property to avoid allocation of an additional field
56+
boundary.fn = (/** @type {{ error?: Error }} */ payload) => {
57+
let { error } = payload;
58+
59+
// In the future, boundaries might handle other things other than errors
60+
if (!error) {
61+
return;
62+
}
63+
64+
var onerror = props.onerror;
65+
let failed_snippet = props.failed;
66+
67+
if (boundary_effect) {
68+
destroy_effect(boundary_effect);
69+
}
70+
71+
// If we have nothing to capture the error then rethrow the error
72+
// for another boundary to handle
73+
if (!onerror && !failed_snippet) {
74+
throw error;
75+
}
76+
77+
// Handle resetting the error boundary
78+
var reset = () => {
79+
if (boundary_effect) {
80+
pause_effect(boundary_effect);
81+
}
82+
with_boundary(boundary, () => {
83+
boundary_effect = null;
84+
boundary_effect = branch(() => boundary_fn(anchor));
85+
});
86+
};
87+
88+
// Handle the `onerror` event handler
89+
if (onerror) {
90+
onerror(error, reset);
91+
}
92+
93+
// Handle the `failed` snippet fallback
94+
if (failed_snippet) {
95+
// Ensure we create the boundary branch after the catch event cycle finishes
96+
queue_micro_task(() => {
97+
with_boundary(boundary, () => {
98+
boundary_effect = null;
99+
boundary_effect = branch(() =>
100+
failed_snippet(
101+
anchor,
102+
() => error,
103+
() => reset
104+
)
105+
);
106+
});
107+
});
108+
}
109+
};
110+
111+
if (hydrating) {
112+
hydrate_next();
113+
}
114+
115+
boundary_effect = branch(() => boundary_fn(anchor));
116+
}, EFFECT_TRANSPARENT | BOUNDARY_EFFECT);
117+
118+
if (hydrating) {
119+
anchor = hydrate_node;
120+
}
121+
}

0 commit comments

Comments
 (0)