feat: provide better error messages in DEV

pull/11526/head
Dominic Gannaway 2 years ago
parent 59f4feb4d8
commit c1180ca69d

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: provide better error messages in DEV

@ -449,7 +449,11 @@ export function client_component(source, analysis, options) {
b.id('import.meta.hot'),
b.block([
b.const(b.id('s'), b.call('$.source', b.id(analysis.name))),
b.const(b.id('filename'), b.member(b.id(analysis.name), b.id('name'))),
b.stmt(b.assignment('=', b.id(analysis.name), b.call('$.hmr', b.id('s')))),
b.stmt(
b.assignment('=', b.member(b.id(analysis.name), b.id('filename')), b.id('filename'))
),
b.if(
b.id('import.meta.hot.acceptExports'),
b.stmt(

@ -232,7 +232,7 @@ function setup_select_synchronization(value_binding, context) {
context.state.init.push(
b.stmt(
b.call(
'$.render_effect',
'$.template_effect',
b.thunk(
b.block([
b.stmt(
@ -448,7 +448,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
* Resulting code for dynamic looks something like this:
* ```js
* let value;
* $.render_effect(() => {
* $.template_effect(() => {
* if (value !== (value = 'new value')) {
* element.property = value;
* // or
@ -1184,7 +1184,7 @@ function serialize_update(statement) {
const body =
statement.type === 'ExpressionStatement' ? statement.expression : b.block([statement]);
return b.stmt(b.call('$.render_effect', b.thunk(body)));
return b.stmt(b.call('$.template_effect', b.thunk(body)));
}
/**
@ -1194,7 +1194,7 @@ function serialize_update(statement) {
function serialize_render_stmt(state) {
return state.update.length === 1
? serialize_update(state.update[0])
: b.stmt(b.call('$.render_effect', b.thunk(b.block(state.update))));
: b.stmt(b.call('$.template_effect', b.thunk(b.block(state.update))));
}
/**
@ -1739,7 +1739,7 @@ export const template_visitors = {
state.init.push(
b.stmt(
b.call(
'$.render_effect',
'$.template_effect',
b.thunk(
b.block([
b.stmt(

@ -90,6 +90,7 @@ export {
legacy_pre_effect,
legacy_pre_effect_reset,
render_effect,
template_effect,
user_effect,
user_pre_effect
} from './reactivity/effects.js';

@ -67,9 +67,10 @@ export function push_effect(effect, parent_effect) {
* @param {number} type
* @param {(() => void | (() => void))} fn
* @param {boolean} sync
* @param {string} [name]
* @returns {import('#client').Effect}
*/
function create_effect(type, fn, sync) {
function create_effect(type, fn, sync, name) {
var is_root = (type & ROOT_EFFECT) !== 0;
/** @type {import('#client').Effect} */
@ -90,6 +91,7 @@ function create_effect(type, fn, sync) {
if (DEV) {
effect.component_function = dev_current_component_function;
effect.name = name;
}
if (current_reaction !== null && !is_root) {
@ -150,7 +152,7 @@ export function user_effect(fn) {
// Non-nested `$effect(...)` in a component should be deferred
// until the component is mounted
const defer =
var defer =
current_effect !== null &&
(current_effect.f & RENDER_EFFECT) !== 0 &&
// TODO do we actually need this? removing them changes nothing
@ -158,10 +160,14 @@ export function user_effect(fn) {
!current_component_context.m;
if (defer) {
const context = /** @type {import('#client').ComponentContext} */ (current_component_context);
var context = /** @type {import('#client').ComponentContext} */ (current_component_context);
(context.e ??= []).push(fn);
} else {
effect(fn);
var signal = effect(fn);
if (DEV) {
signal.name = '$effect';
}
return signal;
}
}
@ -172,7 +178,13 @@ export function user_effect(fn) {
*/
export function user_pre_effect(fn) {
validate_effect('$effect.pre');
return render_effect(fn);
var signal = render_effect(fn);
if (DEV) {
if (DEV) {
signal.name = '$effect.pre';
}
}
return signal;
}
/**
@ -249,6 +261,17 @@ export function render_effect(fn) {
return create_effect(RENDER_EFFECT, fn, true);
}
/**
* @param {() => void | (() => void)} fn
* @returns {import('#client').Effect}
*/
export function template_effect(fn) {
if (DEV) {
return create_effect(RENDER_EFFECT, fn, true, 'Svelte template effect');
}
return render_effect(fn);
}
/**
* @param {(() => void)} fn
* @param {number} flags

@ -51,6 +51,8 @@ export interface Effect extends Reaction {
next: null | Effect;
/** Dev only */
component_function?: any;
/** Dev only */
name?: string;
}
export interface ValueDebug<V = unknown> extends Value<V> {

@ -28,6 +28,9 @@ import { lifecycle_outside_component } from '../shared/errors.js';
const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1;
// Used for DEV time error handling
/** @param {WeakSet<Error>} value */
const handled_errors = new WeakSet();
// Used for controlling the flush of effects.
let current_scheduler_mode = FLUSH_MICROTASK;
// Used for handling scheduling
@ -239,6 +242,57 @@ export function check_dirtiness(reaction) {
return is_dirty;
}
/**
* @param {Error} error
* @param {import("#client").Effect} effect
* @param {import("#client").ComponentContext | null} component_context
*/
function trigger_error_boundary(error, effect, component_context) {
// Given we don't yet have error boundaries, we will just always throw.
if (DEV && !handled_errors.has(error)) {
let effect_name = effect.name;
if (component_context !== null) {
const component_stack = [];
/** @type {import("#client").ComponentContext | null} */
let current_context = component_context;
while (current_context !== null) {
var func = current_context.function;
var filename = func?.filename;
if (filename) {
component_stack.push(filename + (current_context === component_context ? `` : ''));
}
current_context = current_context.p;
}
let component_stack_string =
`Svelte caught an error thrown by <${component_stack.at(-1)}>.` +
` You should fix this error in your code.\n\nError: ${error.message}\n\nThe component tree when this error occured:\n`;
if (effect_name) {
component_stack_string += `\tin ${effect_name}\n`;
}
for (const name of component_stack) {
component_stack_string += `\tin <${name}>\n`;
}
if (error.stack) {
const stack = error.stack.split('\n');
stack.shift();
component_stack_string += `\nThe error is located at:\n${stack.join('\n')}`;
}
const new_error = new Error(component_stack_string);
handled_errors.add(new_error);
throw new_error;
}
}
throw error;
}
/**
* @template V
* @param {import('#client').Reaction} signal
@ -431,6 +485,8 @@ export function execute_effect(effect) {
execute_effect_teardown(effect);
var teardown = execute_reaction_fn(effect);
effect.teardown = typeof teardown === 'function' ? teardown : null;
} catch (error) {
trigger_error_boundary(/** @type {Error} */ (error), effect, current_component_context);
} finally {
current_effect = previous_effect;
current_component_context = previous_component_context;
@ -1108,7 +1164,10 @@ export function pop(component) {
if (effects !== null) {
context_stack_item.e = null;
for (let i = 0; i < effects.length; i++) {
effect(effects[i]);
var signal = effect(effects[i]);
if (DEV) {
signal.name = '$effect';
}
}
}
current_component_context = context_stack_item.p;

@ -15,7 +15,9 @@
"end": 36,
"type": "StyleDirective",
"name": "color",
"modifiers": ["important"],
"modifiers": [
"important"
],
"value": [
{
"type": "MustacheTag",
@ -45,4 +47,4 @@
}
]
}
}
}

@ -9,8 +9,11 @@
"start": 0,
"end": 30,
"data": " svelte-ignore foo bar ",
"ignores": ["foo", "bar"]
"ignores": [
"foo",
"bar"
]
}
]
}
}
}

@ -1116,4 +1116,4 @@
"transparent": false
},
"options": null
}
}

@ -407,4 +407,4 @@
"transparent": false
},
"options": null
}
}

@ -157,4 +157,4 @@
"transparent": false
},
"options": null
}
}

@ -28,6 +28,6 @@ export default function Bind_component_snippet($$anchor) {
var text = $.sibling(node, true);
$.render_effect(() => $.set_text(text, ` value: ${$.stringify($.get(value))}`));
$.template_effect(() => $.set_text(text, ` value: ${$.stringify($.get(value))}`));
$.append($$anchor, fragment_1);
}

@ -13,17 +13,17 @@ export default function Main($$anchor) {
var custom_element = $.sibling($.sibling(svg, true));
var div_1 = $.sibling($.sibling(custom_element, true));
$.render_effect(() => $.set_attribute(div_1, "foobar", y()));
$.template_effect(() => $.set_attribute(div_1, "foobar", y()));
var svg_1 = $.sibling($.sibling(div_1, true));
$.render_effect(() => $.set_attribute(svg_1, "viewBox", y()));
$.template_effect(() => $.set_attribute(svg_1, "viewBox", y()));
var custom_element_1 = $.sibling($.sibling(svg_1, true));
$.render_effect(() => $.set_custom_element_data(custom_element_1, "fooBar", y()));
$.template_effect(() => $.set_custom_element_data(custom_element_1, "fooBar", y()));
$.render_effect(() => {
$.template_effect(() => {
$.set_attribute(div, "foobar", x);
$.set_attribute(svg, "viewBox", x);
$.set_custom_element_data(custom_element, "fooBar", x);

@ -8,7 +8,7 @@ export default function Each_string_template($$anchor) {
$.each(node, 1, () => ['foo', 'bar', 'baz'], $.index, ($$anchor, thing, $$index) => {
var text = $.text($$anchor);
$.render_effect(() => $.set_text(text, `${$.stringify($.unwrap(thing))}, `));
$.template_effect(() => $.set_text(text, `${$.stringify($.unwrap(thing))}, `));
$.append($$anchor, text);
});

@ -19,7 +19,7 @@ export default function Function_prop_no_getter($$anchor) {
children: ($$anchor, $$slotProps) => {
var text = $.text($$anchor);
$.render_effect(() => $.set_text(text, `clicks: ${$.stringify($.get(count))}`));
$.template_effect(() => $.set_text(text, `clicks: ${$.stringify($.get(count))}`));
$.append($$anchor, text);
}
});

@ -11,8 +11,10 @@ function Hmr($$anchor) {
if (import.meta.hot) {
const s = $.source(Hmr);
const filename = Hmr.name;
Hmr = $.hmr(s);
Hmr.filename = filename;
if (import.meta.hot.acceptExports) import.meta.hot.acceptExports(["default"], (module) => {
$.set(s, module.default);
@ -21,4 +23,4 @@ if (import.meta.hot) {
});
}
export default Hmr;
export default Hmr;
Loading…
Cancel
Save