Skip to content

Commit 520d893

Browse files
committed
WIP
2 parents f3b7ce0 + 84e0790 commit 520d893

File tree

14 files changed

+215
-271
lines changed

14 files changed

+215
-271
lines changed

.changeset/early-tips-prove.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: invoke parent boundary of deriveds that throw
Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { BlockStatement, Statement, Expression } from 'estree' */
1+
/** @import { BlockStatement, Statement, Expression, FunctionDeclaration, VariableDeclaration, ArrowFunctionExpression } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import { dev } from '../../../../state.js';
@@ -34,65 +34,61 @@ export function SvelteBoundary(node, context) {
3434
const nodes = [];
3535

3636
/** @type {Statement[]} */
37-
const external_statements = [];
37+
const const_tags = [];
3838

3939
/** @type {Statement[]} */
40-
const internal_statements = [];
40+
const hoisted = [];
4141

42-
const snippets_visits = [];
42+
// const tags need to live inside the boundary, but might also be referenced in hoisted snippets.
43+
// to resolve this we cheat: we duplicate const tags inside snippets
44+
for (const child of node.fragment.nodes) {
45+
if (child.type === 'ConstTag') {
46+
context.visit(child, { ...context.state, init: const_tags });
47+
}
48+
}
4349

44-
// Capture the `failed` implicit snippet prop
4550
for (const child of node.fragment.nodes) {
46-
if (
47-
child.type === 'SnippetBlock' &&
48-
(child.expression.name === 'failed' || child.expression.name === 'pending')
49-
) {
50-
// we need to delay the visit of the snippets in case they access a ConstTag that is declared
51-
// after the snippets so that the visitor for the const tag can be updated
52-
snippets_visits.push(() => {
53-
/** @type {Statement[]} */
54-
const init = [];
55-
context.visit(child, { ...context.state, init });
56-
props.properties.push(b.prop('init', child.expression, child.expression));
57-
external_statements.push(...init);
58-
});
59-
} else if (child.type === 'ConstTag') {
51+
if (child.type === 'ConstTag') {
52+
continue;
53+
}
54+
55+
if (child.type === 'SnippetBlock') {
6056
/** @type {Statement[]} */
61-
const init = [];
62-
context.visit(child, { ...context.state, init });
63-
64-
if (dev) {
65-
// In dev we must separate the declarations from the code
66-
// that eagerly evaluate the expression...
67-
for (const statement of init) {
68-
if (statement.type === 'VariableDeclaration') {
69-
external_statements.push(statement);
70-
} else {
71-
internal_statements.push(statement);
72-
}
73-
}
74-
} else {
75-
external_statements.push(...init);
57+
const statements = [];
58+
59+
context.visit(child, { ...context.state, init: statements });
60+
61+
const snippet = /** @type {VariableDeclaration} */ (statements[0]);
62+
63+
const snippet_fn = dev
64+
? // @ts-expect-error we know this shape is correct
65+
snippet.declarations[0].init.arguments[1]
66+
: snippet.declarations[0].init;
67+
68+
snippet_fn.body.body.unshift(
69+
...const_tags.filter((node) => node.type === 'VariableDeclaration')
70+
);
71+
72+
hoisted.push(snippet);
73+
74+
if (['failed', 'pending'].includes(child.expression.name)) {
75+
props.properties.push(b.prop('init', child.expression, child.expression));
7676
}
77-
} else {
78-
nodes.push(child);
77+
78+
continue;
7979
}
80-
}
8180

82-
snippets_visits.forEach((visit) => visit());
81+
nodes.push(child);
82+
}
8383

8484
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
8585

86-
if (dev && internal_statements.length) {
87-
block.body.unshift(...internal_statements);
88-
}
86+
block.body.unshift(...const_tags);
8987

9088
const boundary = b.stmt(
9189
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))
9290
);
9391

9492
context.state.template.push_comment();
95-
context.state.init.push(
96-
external_statements.length > 0 ? b.block([...external_statements, boundary]) : boundary
97-
);
93+
context.state.init.push(hoisted.length > 0 ? b.block([...hoisted, boundary]) : boundary);
9894
}

packages/svelte/src/internal/client/dom/blocks/boundary.js

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
44
import { component_context, set_component_context } from '../../context.js';
5+
import { invoke_error_boundary } from '../../error-handling.js';
56
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
67
import {
78
active_effect,
89
active_reaction,
9-
handle_error,
1010
set_active_effect,
11-
set_active_reaction,
12-
reset_is_throwing_error
11+
set_active_reaction
1312
} from '../../runtime.js';
1413
import {
1514
hydrate_next,
@@ -129,16 +128,18 @@ export class Boundary {
129128
}
130129
});
131130
} else {
132-
this.#main_effect = branch(() => children(this.#anchor));
131+
try {
132+
this.#main_effect = branch(() => children(this.#anchor));
133+
} catch (error) {
134+
this.error(error);
135+
}
133136

134137
if (this.#pending_count > 0) {
135138
this.#show_pending_snippet();
136139
} else {
137140
this.ran = true;
138141
}
139142
}
140-
141-
reset_is_throwing_error();
142143
}, flags);
143144

144145
if (hydrating) {
@@ -239,12 +240,7 @@ export class Boundary {
239240

240241
this.#main_effect = this.#run(() => {
241242
this.#is_creating_fallback = false;
242-
243-
try {
244-
return branch(() => this.#children(this.#anchor));
245-
} finally {
246-
reset_is_throwing_error();
247-
}
243+
return branch(() => this.#children(this.#anchor));
248244
});
249245

250246
if (this.#pending_count > 0) {
@@ -260,7 +256,14 @@ export class Boundary {
260256
throw error;
261257
}
262258

263-
onerror?.(error, reset);
259+
var previous_reaction = active_reaction;
260+
261+
try {
262+
set_active_reaction(null);
263+
onerror?.(error, reset);
264+
} finally {
265+
set_active_reaction(previous_reaction);
266+
}
264267

265268
if (this.#main_effect) {
266269
destroy_effect(this.#main_effect);
@@ -297,10 +300,9 @@ export class Boundary {
297300
);
298301
});
299302
} catch (error) {
300-
handle_error(error, this.#effect, null, this.#effect.ctx);
303+
invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent));
301304
return null;
302305
} finally {
303-
reset_is_throwing_error();
304306
this.#is_creating_fallback = false;
305307
}
306308
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/** @import { Effect } from '#client' */
2+
/** @import { Boundary } from './dom/blocks/boundary.js' */
3+
import { DEV } from 'esm-env';
4+
import { FILENAME } from '../../constants.js';
5+
import { is_firefox } from './dom/operations.js';
6+
import { BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js';
7+
import { define_property } from '../shared/utils.js';
8+
import { active_effect } from './runtime.js';
9+
10+
/**
11+
* @param {unknown} error
12+
*/
13+
export function handle_error(error) {
14+
var effect = /** @type {Effect} */ (active_effect);
15+
16+
if (DEV && error instanceof Error) {
17+
// adjust_error(error, effect);
18+
}
19+
20+
if ((effect.f & EFFECT_RAN) === 0) {
21+
// if the error occurred while creating this subtree, we let it
22+
// bubble up until it hits a boundary that can handle it
23+
if ((effect.f & BOUNDARY_EFFECT) === 0) {
24+
throw error;
25+
}
26+
27+
// @ts-expect-error
28+
effect.fn(error);
29+
} else {
30+
// otherwise we bubble up the effect tree ourselves
31+
invoke_error_boundary(error, effect);
32+
}
33+
}
34+
35+
/**
36+
* @param {unknown} error
37+
* @param {Effect | null} effect
38+
*/
39+
export function invoke_error_boundary(error, effect) {
40+
while (effect !== null) {
41+
if ((effect.f & BOUNDARY_EFFECT) !== 0) {
42+
try {
43+
/** @type {Boundary} */ (effect.b).error(error);
44+
return;
45+
} catch {}
46+
}
47+
48+
effect = effect.parent;
49+
}
50+
51+
throw error;
52+
}
53+
54+
/** @type {WeakSet<Error>} */
55+
const adjusted_errors = new WeakSet();
56+
57+
/**
58+
* Add useful information to the error message/stack in development
59+
* @param {Error} error
60+
* @param {Effect} effect
61+
*/
62+
function adjust_error(error, effect) {
63+
if (adjusted_errors.has(error)) return;
64+
adjusted_errors.add(error);
65+
66+
var indent = is_firefox ? ' ' : '\t';
67+
var component_stack = `\n${indent}in ${effect.fn?.name || '<unknown>'}`;
68+
var context = effect.ctx;
69+
70+
while (context !== null) {
71+
component_stack += `\n${indent}in ${context.function?.[FILENAME].split('/').pop()}`;
72+
context = context.p;
73+
}
74+
75+
define_property(error, 'message', {
76+
value: error.message + `\n${component_stack}\n`
77+
});
78+
79+
if (error.stack) {
80+
// Filter out internal modules
81+
define_property(error, 'stack', {
82+
value: error.stack
83+
.split('\n')
84+
.filter((line) => !line.includes('svelte/src/internal'))
85+
.join('\n')
86+
});
87+
}
88+
}

packages/svelte/src/internal/client/reactivity/deriveds.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { get_pending_boundary } from '../dom/blocks/boundary.js';
3535
import { component_context } from '../context.js';
3636
import { UNINITIALIZED } from '../../../constants.js';
3737
import { current_batch } from './batch.js';
38+
import { handle_error } from '../error-handling.js';
3839

3940
/** @type {Effect | null} */
4041
export let from_async_derived = null;

packages/svelte/src/internal/client/reactivity/effects.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -367,13 +367,6 @@ export function template_effect(fn, sync = [], async = [], d = derived) {
367367
*/
368368
function create_template_effect(fn, deriveds) {
369369
var effect = () => fn(...deriveds.map(get));
370-
371-
if (DEV) {
372-
define_property(effect, 'name', {
373-
value: '{expression}'
374-
});
375-
}
376-
377370
create_effect(RENDER_EFFECT, effect, true);
378371
}
379372

@@ -461,7 +454,11 @@ export function destroy_block_effect_children(signal) {
461454
export function destroy_effect(effect, remove_dom = true) {
462455
var removed = false;
463456

464-
if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes_start !== null) {
457+
if (
458+
(remove_dom || (effect.f & HEAD_EFFECT) !== 0) &&
459+
effect.nodes_start !== null &&
460+
effect.nodes_end !== null
461+
) {
465462
remove_effect_dom(effect.nodes_start, /** @type {TemplateNode} */ (effect.nodes_end));
466463
removed = true;
467464
}

0 commit comments

Comments
 (0)