Skip to content

Commit fcc72ae

Browse files
trueadmRich-Harris
andauthored
feat: provide better error messages in DEV (#11526)
* feat: provide better error messages in DEV * fix stuff * fix stuff * fix tests * fix * assert.include results in better errors on mismatches * remove indentation * tweak * rename * fix issues * more fixes * more fixes * neaten up stack trace * Update packages/svelte/src/internal/client/reactivity/effects.js Co-authored-by: Rich Harris <[email protected]> * Update packages/svelte/src/internal/client/runtime.js Co-authored-by: Rich Harris <[email protected]> * address feedback * lint --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 641e411 commit fcc72ae

File tree

16 files changed

+144
-32
lines changed

16 files changed

+144
-32
lines changed

.changeset/hungry-pants-push.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+
feat: provide better error messages in DEV

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -449,13 +449,19 @@ export function client_component(source, analysis, options) {
449449
b.id('import.meta.hot'),
450450
b.block([
451451
b.const(b.id('s'), b.call('$.source', b.id(analysis.name))),
452+
b.const(b.id('filename'), b.member(b.id(analysis.name), b.id('filename'))),
452453
b.stmt(b.assignment('=', b.id(analysis.name), b.call('$.hmr', b.id('s')))),
454+
b.stmt(
455+
b.assignment('=', b.member(b.id(analysis.name), b.id('filename')), b.id('filename'))
456+
),
453457
b.if(
454458
b.id('import.meta.hot.acceptExports'),
455-
b.stmt(
456-
b.call('import.meta.hot.acceptExports', b.array([b.literal('default')]), accept_fn)
457-
),
458-
b.stmt(b.call('import.meta.hot.accept', accept_fn))
459+
b.block([
460+
b.stmt(
461+
b.call('import.meta.hot.acceptExports', b.array([b.literal('default')]), accept_fn)
462+
)
463+
]),
464+
b.block([b.stmt(b.call('import.meta.hot.accept', accept_fn))])
459465
)
460466
])
461467
),

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ function setup_select_synchronization(value_binding, context) {
232232
context.state.init.push(
233233
b.stmt(
234234
b.call(
235-
'$.render_effect',
235+
'$.template_effect',
236236
b.thunk(
237237
b.block([
238238
b.stmt(
@@ -448,7 +448,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
448448
* Resulting code for dynamic looks something like this:
449449
* ```js
450450
* let value;
451-
* $.render_effect(() => {
451+
* $.template_effect(() => {
452452
* if (value !== (value = 'new value')) {
453453
* element.property = value;
454454
* // or
@@ -1184,7 +1184,7 @@ function serialize_update(statement) {
11841184
const body =
11851185
statement.type === 'ExpressionStatement' ? statement.expression : b.block([statement]);
11861186

1187-
return b.stmt(b.call('$.render_effect', b.thunk(body)));
1187+
return b.stmt(b.call('$.template_effect', b.thunk(body)));
11881188
}
11891189

11901190
/**
@@ -1194,7 +1194,7 @@ function serialize_update(statement) {
11941194
function serialize_render_stmt(state) {
11951195
return state.update.length === 1
11961196
? serialize_update(state.update[0])
1197-
: b.stmt(b.call('$.render_effect', b.thunk(b.block(state.update))));
1197+
: b.stmt(b.call('$.template_effect', b.thunk(b.block(state.update))));
11981198
}
11991199

12001200
/**
@@ -1739,7 +1739,7 @@ export const template_visitors = {
17391739
state.init.push(
17401740
b.stmt(
17411741
b.call(
1742-
'$.render_effect',
1742+
'$.template_effect',
17431743
b.thunk(
17441744
b.block([
17451745
b.stmt(

packages/svelte/src/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ export const disallowed_paragraph_contents = [
143143
'pre',
144144
'section',
145145
'table',
146-
'ul'
146+
'ul',
147+
'p'
147148
];
148149

149150
// https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export {
9090
legacy_pre_effect,
9191
legacy_pre_effect_reset,
9292
render_effect,
93+
template_effect,
9394
user_effect,
9495
user_pre_effect
9596
} from './reactivity/effects.js';

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { set } from './sources.js';
3434
import { remove } from '../dom/reconciler.js';
3535
import * as e from '../errors.js';
3636
import { DEV } from 'esm-env';
37+
import { define_property } from '../utils.js';
3738

3839
/**
3940
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@@ -150,18 +151,25 @@ export function user_effect(fn) {
150151

151152
// Non-nested `$effect(...)` in a component should be deferred
152153
// until the component is mounted
153-
const defer =
154+
var defer =
154155
current_effect !== null &&
155156
(current_effect.f & RENDER_EFFECT) !== 0 &&
156157
// TODO do we actually need this? removing them changes nothing
157158
current_component_context !== null &&
158159
!current_component_context.m;
159160

161+
if (DEV) {
162+
define_property(fn, 'name', {
163+
value: '$effect'
164+
});
165+
}
166+
160167
if (defer) {
161-
const context = /** @type {import('#client').ComponentContext} */ (current_component_context);
168+
var context = /** @type {import('#client').ComponentContext} */ (current_component_context);
162169
(context.e ??= []).push(fn);
163170
} else {
164-
effect(fn);
171+
var signal = effect(fn);
172+
return signal;
165173
}
166174
}
167175

@@ -172,6 +180,11 @@ export function user_effect(fn) {
172180
*/
173181
export function user_pre_effect(fn) {
174182
validate_effect('$effect.pre');
183+
if (DEV) {
184+
define_property(fn, 'name', {
185+
value: '$effect.pre'
186+
});
187+
}
175188
return render_effect(fn);
176189
}
177190

@@ -249,6 +262,19 @@ export function render_effect(fn) {
249262
return create_effect(RENDER_EFFECT, fn, true);
250263
}
251264

265+
/**
266+
* @param {() => void | (() => void)} fn
267+
* @returns {import('#client').Effect}
268+
*/
269+
export function template_effect(fn) {
270+
if (DEV) {
271+
define_property(fn, 'name', {
272+
value: '{expression}'
273+
});
274+
}
275+
return render_effect(fn);
276+
}
277+
252278
/**
253279
* @param {(() => void)} fn
254280
* @param {number} flags

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,15 @@ export function hydrate(component, options) {
167167
return instance;
168168
}, false);
169169
} catch (error) {
170-
if (!hydrated && options.recover !== false) {
170+
if (
171+
!hydrated &&
172+
options.recover !== false &&
173+
/** @type {Error} */ (error).message.includes('hydration_missing_marker_close')
174+
) {
171175
w.hydration_mismatch();
172176

177+
// If an error occured above, the operations might not yet have been initialised.
178+
init_operations();
173179
clear_text_content(target);
174180

175181
set_hydrating(false);

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

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import { lifecycle_outside_component } from '../shared/errors.js';
2828
const FLUSH_MICROTASK = 0;
2929
const FLUSH_SYNC = 1;
3030

31+
// Used for DEV time error handling
32+
/** @param {WeakSet<Error>} value */
33+
const handled_errors = new WeakSet();
3134
// Used for controlling the flush of effects.
3235
let current_scheduler_mode = FLUSH_MICROTASK;
3336
// Used for handling scheduling
@@ -239,6 +242,62 @@ export function check_dirtiness(reaction) {
239242
return is_dirty;
240243
}
241244

245+
/**
246+
* @param {Error} error
247+
* @param {import("#client").Effect} effect
248+
* @param {import("#client").ComponentContext | null} component_context
249+
*/
250+
function handle_error(error, effect, component_context) {
251+
// Given we don't yet have error boundaries, we will just always throw.
252+
if (!DEV || handled_errors.has(error) || component_context === null) {
253+
throw error;
254+
}
255+
256+
const component_stack = [];
257+
258+
const effect_name = effect.fn.name;
259+
260+
if (effect_name) {
261+
component_stack.push(effect_name);
262+
}
263+
264+
/** @type {import("#client").ComponentContext | null} */
265+
let current_context = component_context;
266+
267+
while (current_context !== null) {
268+
var filename = current_context.function?.filename;
269+
270+
if (filename) {
271+
const file = filename.split('/').at(-1);
272+
component_stack.push(file);
273+
}
274+
275+
current_context = current_context.p;
276+
}
277+
278+
const indent = /Firefox/.test(navigator.userAgent) ? ' ' : '\t';
279+
error.message += `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`;
280+
281+
const stack = error.stack;
282+
283+
// Filter out internal files from callstack
284+
if (stack) {
285+
const lines = stack.split('\n');
286+
const new_lines = [];
287+
for (let i = 0; i < lines.length; i++) {
288+
const line = lines[i];
289+
if (line.includes('svelte/src/internal')) {
290+
continue;
291+
}
292+
new_lines.push(line);
293+
}
294+
error.stack = new_lines.join('\n');
295+
}
296+
297+
handled_errors.add(error);
298+
throw error;
299+
}
300+
242301
/**
243302
* @template V
244303
* @param {import('#client').Reaction} signal
@@ -260,7 +319,7 @@ export function execute_reaction_fn(signal) {
260319
current_untracking = false;
261320

262321
try {
263-
let res = signal.fn();
322+
let res = (0, signal.fn)();
264323
let dependencies = /** @type {import('#client').Value<unknown>[]} **/ (signal.deps);
265324
if (current_dependencies !== null) {
266325
let i;
@@ -431,6 +490,8 @@ export function execute_effect(effect) {
431490
execute_effect_teardown(effect);
432491
var teardown = execute_reaction_fn(effect);
433492
effect.teardown = typeof teardown === 'function' ? teardown : null;
493+
} catch (error) {
494+
handle_error(/** @type {Error} */ (error), effect, current_component_context);
434495
} finally {
435496
current_effect = previous_effect;
436497
current_component_context = previous_component_context;

packages/svelte/tests/runtime-legacy/samples/component-not-constructor2-dev/_config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default test({
1515
component.componentName = 'banana';
1616
throw new Error('Expected an error');
1717
} catch (err) {
18-
assert.equal(
18+
assert.include(
1919
/** @type {Error} */ (err).message,
2020
'svelte_component_invalid_this_value\nThe `this={...}` property of a `<svelte:component>` must be a Svelte component, if defined'
2121
);

packages/svelte/tests/runtime-legacy/samples/component-not-constructor2/_config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default test({
1212
component.componentName = 'banana';
1313
throw new Error('Expected an error');
1414
} catch (err) {
15-
assert.equal(/** @type {Error} */ (err).message, '$$component is not a function');
15+
assert.include(/** @type {Error} */ (err).message, '$$component is not a function');
1616
}
1717
}
1818
});

packages/svelte/tests/runtime-legacy/shared.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,9 +392,9 @@ async function run_test_variant(
392392
}
393393
} catch (err) {
394394
if (config.runtime_error) {
395-
assert.equal((err as Error).message, config.runtime_error);
395+
assert.include((err as Error).message, config.runtime_error);
396396
} else if (config.error && !unintended_error) {
397-
assert.equal((err as Error).message, config.error);
397+
assert.include((err as Error).message, config.error);
398398
} else {
399399
throw err;
400400
}

packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ export default function Bind_component_snippet($$anchor) {
2828

2929
var text = $.sibling(node, true);
3030

31-
$.render_effect(() => $.set_text(text, ` value: ${$.stringify($.get(value))}`));
31+
$.template_effect(() => $.set_text(text, ` value: ${$.stringify($.get(value))}`));
3232
$.append($$anchor, fragment_1);
3333
}

packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ export default function Main($$anchor) {
1313
var custom_element = $.sibling($.sibling(svg, true));
1414
var div_1 = $.sibling($.sibling(custom_element, true));
1515

16-
$.render_effect(() => $.set_attribute(div_1, "foobar", y()));
16+
$.template_effect(() => $.set_attribute(div_1, "foobar", y()));
1717

1818
var svg_1 = $.sibling($.sibling(div_1, true));
1919

20-
$.render_effect(() => $.set_attribute(svg_1, "viewBox", y()));
20+
$.template_effect(() => $.set_attribute(svg_1, "viewBox", y()));
2121

2222
var custom_element_1 = $.sibling($.sibling(svg_1, true));
2323

24-
$.render_effect(() => $.set_custom_element_data(custom_element_1, "fooBar", y()));
24+
$.template_effect(() => $.set_custom_element_data(custom_element_1, "fooBar", y()));
2525

26-
$.render_effect(() => {
26+
$.template_effect(() => {
2727
$.set_attribute(div, "foobar", x);
2828
$.set_attribute(svg, "viewBox", x);
2929
$.set_custom_element_data(custom_element, "fooBar", x);

packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default function Each_string_template($$anchor) {
88
$.each(node, 1, () => ['foo', 'bar', 'baz'], $.index, ($$anchor, thing, $$index) => {
99
var text = $.text($$anchor);
1010

11-
$.render_effect(() => $.set_text(text, `${$.stringify($.unwrap(thing))}, `));
11+
$.template_effect(() => $.set_text(text, `${$.stringify($.unwrap(thing))}, `));
1212
$.append($$anchor, text);
1313
});
1414

packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default function Function_prop_no_getter($$anchor) {
1919
children: ($$anchor, $$slotProps) => {
2020
var text = $.text($$anchor);
2121

22-
$.render_effect(() => $.set_text(text, `clicks: ${$.stringify($.get(count))}`));
22+
$.template_effect(() => $.set_text(text, `clicks: ${$.stringify($.get(count))}`));
2323
$.append($$anchor, text);
2424
}
2525
});

packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@ function Hmr($$anchor) {
1111

1212
if (import.meta.hot) {
1313
const s = $.source(Hmr);
14+
const filename = Hmr.filename;
1415

1516
Hmr = $.hmr(s);
17+
Hmr.filename = filename;
1618

17-
if (import.meta.hot.acceptExports) import.meta.hot.acceptExports(["default"], (module) => {
18-
$.set(s, module.default);
19-
}); else import.meta.hot.accept((module) => {
20-
$.set(s, module.default);
21-
});
19+
if (import.meta.hot.acceptExports) {
20+
import.meta.hot.acceptExports(["default"], (module) => {
21+
$.set(s, module.default);
22+
});
23+
} else {
24+
import.meta.hot.accept((module) => {
25+
$.set(s, module.default);
26+
});
27+
}
2228
}
2329

24-
export default Hmr;
30+
export default Hmr;

0 commit comments

Comments
 (0)