Skip to content

Commit 578fbcd

Browse files
committed
feat: add error boundary support
1 parent e6dd871 commit 578fbcd

File tree

15 files changed

+301
-23
lines changed

15 files changed

+301
-23
lines changed

packages/svelte/elements.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2026,6 +2026,9 @@ export interface SvelteHTMLElements {
20262026
[name: string]: any;
20272027
};
20282028
'svelte:head': { [name: string]: any };
2029+
'svelte:boundary': {
2030+
onerror?: (error: Error, retry: () => void) => void;
2031+
};
20292032

20302033
[name: string]: { [name: string]: any };
20312034
}

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: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/** @import { BlockStatement, Statement, Pattern, 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 {Expression} */
15+
let failed_param = b.literal(null);
16+
/** @type {Expression | null} */
17+
let onerror = null;
18+
19+
for (const attribute of node.attributes) {
20+
if (
21+
attribute.type === 'Attribute' &&
22+
attribute.name === 'onerror' &&
23+
attribute.value !== true &&
24+
!Array.isArray(attribute.value)
25+
) {
26+
onerror = /** @type {Expression} */ (
27+
context.visit(attribute.value.expression, context.state)
28+
);
29+
}
30+
}
31+
32+
for (const child of node.fragment.nodes) {
33+
if (child.type === 'SnippetBlock' && child.expression.name === 'failed') {
34+
/** @type {Statement[]} */
35+
const init = [];
36+
const block_state = { ...context.state, init };
37+
context.visit(child, block_state);
38+
failed_param = b.id('failed');
39+
snippet_statements.push(...init);
40+
} else {
41+
nodes.push(child);
42+
}
43+
}
44+
45+
const block = /** @type {BlockStatement} */ (
46+
context.visit(
47+
{
48+
...node.fragment,
49+
nodes
50+
},
51+
{ ...context.state }
52+
)
53+
);
54+
55+
const boundary = b.stmt(
56+
b.call(
57+
'$.boundary',
58+
context.state.node,
59+
b.arrow([b.id('$$anchor')], block),
60+
failed_param,
61+
!onerror ? b.literal(null) : onerror
62+
)
63+
);
64+
65+
context.state.template.push('<!>');
66+
context.state.init.push(
67+
snippet_statements ? b.block([...snippet_statements, boundary]) : boundary
68+
);
69+
}

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: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/** @import { Effect, Source, TemplateNode, } from '#client' */
2+
/** @import { Snippet } from 'svelte' */
3+
4+
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js';
5+
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
6+
import {
7+
active_effect,
8+
active_reaction,
9+
component_context,
10+
set_active_effect,
11+
set_active_reaction,
12+
set_component_context
13+
} from '../../runtime.js';
14+
import {
15+
hydrate_next,
16+
hydrate_node,
17+
hydrating,
18+
remove_nodes,
19+
set_hydrate_node
20+
} from '../hydration.js';
21+
import { queue_micro_task } from '../task.js';
22+
23+
/**
24+
* @param {Effect} boundary
25+
* @param {() => void} fn
26+
*/
27+
function with_boundary(boundary, fn) {
28+
var previous_effect = active_effect;
29+
var previous_reaction = active_reaction;
30+
var previous_ctx = component_context;
31+
set_active_effect(boundary);
32+
set_active_reaction(boundary);
33+
set_component_context(boundary.ctx);
34+
try {
35+
fn();
36+
} finally {
37+
set_active_effect(previous_effect);
38+
set_active_reaction(previous_reaction);
39+
set_component_context(previous_ctx);
40+
}
41+
}
42+
43+
/**
44+
* @param {TemplateNode} node
45+
* @param {((anchor: Node) => void)} boundary_fn
46+
* @param {(anchor: Node, error: () => Error, retry: () => () => void) => void | null} failed
47+
* @param {(error: Error, retry: () => void) => void | null} onerror
48+
* @returns {void}
49+
*/
50+
export function boundary(node, boundary_fn, failed, onerror) {
51+
var anchor = node;
52+
53+
/** @type {Effect | null} */
54+
var boundary_effect;
55+
56+
block(() => {
57+
var boundary = /** @type {Effect} */ (active_effect);
58+
var start = hydrate_node;
59+
60+
boundary.fn = (/** @type {any} */ payload) => {
61+
var { error } = payload;
62+
63+
// In the future, boundaries might handle other things other than errors
64+
if (!error) {
65+
return;
66+
}
67+
68+
// If we have nothing to capture the error then rethrow the error
69+
// for another boundary to handle
70+
if (!onerror && !failed) {
71+
throw error;
72+
}
73+
74+
// Handle retrying the error boundary
75+
var retry = () => {
76+
if (boundary_effect) {
77+
pause_effect(boundary_effect);
78+
}
79+
with_boundary(boundary, () => {
80+
boundary_effect = null;
81+
boundary_effect = branch(() => boundary_fn(anchor));
82+
});
83+
};
84+
85+
if (onerror) {
86+
onerror(error, retry);
87+
}
88+
89+
if (failed) {
90+
// Ensure we create the boundary branch after the catch event cycle finishes
91+
queue_micro_task(() => {
92+
with_boundary(boundary, () => {
93+
boundary_effect = null;
94+
boundary_effect = branch(() =>
95+
failed(
96+
anchor,
97+
() => error,
98+
() => retry
99+
)
100+
);
101+
});
102+
});
103+
104+
// If there was an error then we'll need to remove the current content
105+
// that was SSR'd
106+
if (hydrating) {
107+
set_hydrate_node(/** @type {TemplateNode} */ (start.nextSibling));
108+
var hydration_node = remove_nodes();
109+
set_hydrate_node(hydration_node);
110+
}
111+
if (boundary_effect) {
112+
destroy_effect(boundary_effect);
113+
}
114+
}
115+
};
116+
117+
if (hydrating) {
118+
hydrate_next();
119+
}
120+
121+
boundary_effect = branch(() => boundary_fn(anchor));
122+
}, EFFECT_TRANSPARENT | BOUNDARY_EFFECT);
123+
124+
if (hydrating) {
125+
anchor = hydrate_node;
126+
}
127+
}

0 commit comments

Comments
 (0)