chore: markdown runtime errors/warnings (#11304)

* chore: markdown runtime warnings

* on second thoughts

* start adding errors too

* lint

* centralise
pull/11311/head
Rich Harris 8 months ago committed by GitHub
parent 880886061d
commit 94b4268ce3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5,6 +5,9 @@ packages/**/config/*.js
packages/svelte/messages/**/*.md packages/svelte/messages/**/*.md
packages/svelte/src/compiler/errors.js packages/svelte/src/compiler/errors.js
packages/svelte/src/compiler/warnings.js packages/svelte/src/compiler/warnings.js
packages/svelte/src/internal/client/errors.js
packages/svelte/src/internal/client/warnings.js
packages/svelte/src/internal/shared/warnings.js
packages/svelte/tests/**/*.svelte packages/svelte/tests/**/*.svelte
packages/svelte/tests/**/_expected* packages/svelte/tests/**/_expected*
packages/svelte/tests/**/_actual* packages/svelte/tests/**/_actual*

@ -35,6 +35,9 @@ export default [
'**/tests', '**/tests',
'packages/svelte/scripts/process-messages/templates/*.js', 'packages/svelte/scripts/process-messages/templates/*.js',
'packages/svelte/src/compiler/errors.js', 'packages/svelte/src/compiler/errors.js',
'packages/svelte/src/internal/client/errors.js',
'packages/svelte/src/internal/client/warnings.js',
'packages/svelte/src/internal/shared/warnings.js',
'packages/svelte/compiler/index.js', 'packages/svelte/compiler/index.js',
// documentation can contain invalid examples // documentation can contain invalid examples
'documentation', 'documentation',

@ -0,0 +1,3 @@
## effect_update_depth_exceeded
Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops

@ -0,0 +1,7 @@
## lifecycle_outside_component
`%name%(...)` can only be used during component initialisation
## lifecycle_legacy_only
`%name%(...)` cannot be used in runes mode

@ -0,0 +1,7 @@
## lifecycle_double_unmount
Tried to unmount a component that was not mounted
## ownership_invalid_binding
%parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%

@ -0,0 +1,3 @@
## dynamic_void_element_content
`<svelte:element this="%tag%">` is a void element — it cannot have content

@ -220,3 +220,7 @@ function transform(name, dest) {
transform('compile-errors', 'src/compiler/errors.js'); transform('compile-errors', 'src/compiler/errors.js');
transform('compile-warnings', 'src/compiler/warnings.js'); transform('compile-warnings', 'src/compiler/warnings.js');
transform('client-warnings', 'src/internal/client/warnings.js');
transform('client-errors', 'src/internal/client/errors.js');
transform('shared-warnings', 'src/internal/shared/warnings.js');

@ -0,0 +1,17 @@
import { DEV } from 'esm-env';
/**
* MESSAGE
* @param {string} PARAMETER
* @returns {never}
*/
export function CODE(PARAMETER) {
if (DEV) {
const error = new Error(`${'CODE'}\n${MESSAGE}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error('CODE');
}
}

@ -0,0 +1,17 @@
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* MESSAGE
* @param {string} PARAMETER
*/
export function CODE(PARAMETER) {
if (DEV) {
console.warn(`%c[svelte] ${'CODE'}\n%c${MESSAGE}`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn('CODE');
}
}

@ -0,0 +1,19 @@
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* MESSAGE
* @param {boolean} trace
* @param {string} PARAMETER
*/
export function CODE(trace, PARAMETER) {
if (DEV) {
console.warn(`%c[svelte] ${'CODE'}\n%c${MESSAGE}`, bold, normal);
if (trace) console.trace('stack trace');
} else {
// TODO print a link to the documentation
console.warn('CODE');
}
}

@ -1,6 +1,7 @@
import { current_component_context, flush_sync, untrack } from './internal/client/runtime.js'; import { current_component_context, flush_sync, untrack } from './internal/client/runtime.js';
import { is_array } from './internal/client/utils.js'; import { is_array } from './internal/client/utils.js';
import { user_effect } from './internal/client/index.js'; import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js';
/** /**
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
@ -18,7 +19,7 @@ import { user_effect } from './internal/client/index.js';
*/ */
export function onMount(fn) { export function onMount(fn) {
if (current_component_context === null) { if (current_component_context === null) {
throw new Error('onMount can only be used during component initialisation.'); e.lifecycle_outside_component('onMount');
} }
if (current_component_context.l !== null) { if (current_component_context.l !== null) {
@ -43,7 +44,7 @@ export function onMount(fn) {
*/ */
export function onDestroy(fn) { export function onDestroy(fn) {
if (current_component_context === null) { if (current_component_context === null) {
throw new Error('onDestroy can only be used during component initialisation.'); e.lifecycle_outside_component('onDestroy');
} }
onMount(() => () => untrack(fn)); onMount(() => () => untrack(fn));
@ -87,7 +88,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
export function createEventDispatcher() { export function createEventDispatcher() {
const component_context = current_component_context; const component_context = current_component_context;
if (component_context === null) { if (component_context === null) {
throw new Error('createEventDispatcher can only be used during component initialisation.'); e.lifecycle_outside_component('createEventDispatcher');
} }
return (type, detail, options) => { return (type, detail, options) => {
@ -126,7 +127,7 @@ export function createEventDispatcher() {
*/ */
export function beforeUpdate(fn) { export function beforeUpdate(fn) {
if (current_component_context === null) { if (current_component_context === null) {
throw new Error('beforeUpdate can only be used during component initialisation'); e.lifecycle_outside_component('beforeUpdate');
} }
if (current_component_context.l === null) { if (current_component_context.l === null) {
@ -150,11 +151,11 @@ export function beforeUpdate(fn) {
*/ */
export function afterUpdate(fn) { export function afterUpdate(fn) {
if (current_component_context === null) { if (current_component_context === null) {
throw new Error('afterUpdate can only be used during component initialisation.'); e.lifecycle_outside_component('afterUpdate');
} }
if (current_component_context.l === null) { if (current_component_context.l === null) {
throw new Error('afterUpdate cannot be used in runes mode'); e.lifecycle_legacy_only('afterUpdate');
} }
init_update_callbacks(current_component_context).a.push(fn); init_update_callbacks(current_component_context).a.push(fn);

@ -4,6 +4,7 @@ import { STATE_SYMBOL } from '../constants.js';
import { render_effect } from '../reactivity/effects.js'; import { render_effect } from '../reactivity/effects.js';
import { current_component_context, untrack } from '../runtime.js'; import { current_component_context, untrack } from '../runtime.js';
import { get_prototype_of } from '../utils.js'; import { get_prototype_of } from '../utils.js';
import * as w from '../warnings.js';
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */ /** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
const boundaries = {}; const boundaries = {};
@ -115,10 +116,7 @@ export function add_owner(object, owner, global = false) {
let original = get_owner(metadata); let original = get_owner(metadata);
if (owner.filename !== component.filename) { if (owner.filename !== component.filename) {
let message = `${component.filename} passed a value to ${owner.filename} with \`bind:\`, but the value is owned by ${original.filename}. Consider creating a binding between ${original.filename} and ${component.filename}`; w.ownership_invalid_binding(component.filename, owner.filename, original.filename);
// eslint-disable-next-line no-console
console.warn(message);
} }
} }
} }
@ -234,6 +232,7 @@ export function check_ownership(metadata) {
`${component.filename} mutated a value owned by ${original.filename}. This is strongly discouraged` `${component.filename} mutated a value owned by ${original.filename}. This is strongly discouraged`
: 'Mutating a value outside the component that created it is strongly discouraged'; : 'Mutating a value outside the component that created it is strongly discouraged';
// TODO get rid of this, but implement message overloads first
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn( console.warn(
`${message}. Consider passing values to child components with \`bind:\`, or use a callback instead.` `${message}. Consider passing values to child components with \`bind:\`, or use a callback instead.`

@ -0,0 +1,53 @@
/* This file is generated by scripts/process-messages.js. Do not edit! */
import { DEV } from 'esm-env';
/**
* Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
* @returns {never}
*/
export function effect_update_depth_exceeded() {
if (DEV) {
const error = new Error(`${"effect_update_depth_exceeded"}\n${"Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops"}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("effect_update_depth_exceeded");
}
}
/**
* `%name%(...)` can only be used during component initialisation
* @param {string} name
* @returns {never}
*/
export function lifecycle_outside_component(name) {
if (DEV) {
const error = new Error(`${"lifecycle_outside_component"}\n${`\`${name}(...)\` can only be used during component initialisation`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("lifecycle_outside_component");
}
}
/**
* `%name%(...)` cannot be used in runes mode
* @param {string} name
* @returns {never}
*/
export function lifecycle_legacy_only(name) {
if (DEV) {
const error = new Error(`${"lifecycle_legacy_only"}\n${`\`${name}(...)\` cannot be used in runes mode`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("lifecycle_legacy_only");
}
}

@ -19,6 +19,7 @@ import {
import { array_from } from './utils.js'; import { array_from } from './utils.js';
import { handle_event_propagation } from './dom/elements/events.js'; import { handle_event_propagation } from './dom/elements/events.js';
import { reset_head_anchor } from './dom/blocks/svelte-head.js'; import { reset_head_anchor } from './dom/blocks/svelte-head.js';
import * as w from './warnings.js';
/** @type {Set<string>} */ /** @type {Set<string>} */
export const all_registered_events = new Set(); export const all_registered_events = new Set();
@ -269,6 +270,7 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
target.removeEventListener(event_name, bound_event_listener); target.removeEventListener(event_name, bound_event_listener);
} }
root_event_handles.delete(event_handle); root_event_handles.delete(event_handle);
mounted_components.delete(component);
}; };
}); });
@ -289,8 +291,9 @@ let mounted_components = new WeakMap();
export function unmount(component) { export function unmount(component) {
const fn = mounted_components.get(component); const fn = mounted_components.get(component);
if (DEV && !fn) { if (DEV && !fn) {
w.lifecycle_double_unmount();
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('Tried to unmount a component that was not mounted.'); console.trace('stack trace');
} }
fn?.(); fn?.();
} }

@ -22,6 +22,7 @@ import { add_owner } from './dev/ownership.js';
import { mutate, set, source } from './reactivity/sources.js'; import { mutate, set, source } from './reactivity/sources.js';
import { update_derived } from './reactivity/deriveds.js'; import { update_derived } from './reactivity/deriveds.js';
import { inspect_captured_signals, inspect_fn, set_inspect_fn } from './dev/inspect.js'; import { inspect_captured_signals, inspect_fn, set_inspect_fn } from './dev/inspect.js';
import * as e from './errors.js';
const FLUSH_MICROTASK = 0; const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1; const FLUSH_SYNC = 1;
@ -412,13 +413,7 @@ export function execute_effect(effect) {
function infinite_loop_guard() { function infinite_loop_guard() {
if (flush_count > 1000) { if (flush_count > 1000) {
flush_count = 0; flush_count = 0;
throw new Error( e.effect_update_depth_exceeded();
'ERR_SVELTE_TOO_MANY_UPDATES' +
(DEV
? ': Maximum update depth exceeded. This can happen when a reactive block or effect ' +
'repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops.'
: '')
);
} }
flush_count++; flush_count++;
} }
@ -880,12 +875,12 @@ export function is_signal(val) {
* @returns {T} * @returns {T}
*/ */
export function getContext(key) { export function getContext(key) {
const context_map = get_or_init_context_map(); const context_map = get_or_init_context_map('getContext');
const result = /** @type {T} */ (context_map.get(key)); const result = /** @type {T} */ (context_map.get(key));
if (DEV) { if (DEV) {
// @ts-expect-error // @ts-expect-error
const fn = current_component_context?.function; const fn = current_component_context.function;
if (fn) { if (fn) {
add_owner(result, fn, true); add_owner(result, fn, true);
} }
@ -908,7 +903,7 @@ export function getContext(key) {
* @returns {T} * @returns {T}
*/ */
export function setContext(key, context) { export function setContext(key, context) {
const context_map = get_or_init_context_map(); const context_map = get_or_init_context_map('setContext');
context_map.set(key, context); context_map.set(key, context);
return context; return context;
} }
@ -922,7 +917,7 @@ export function setContext(key, context) {
* @returns {boolean} * @returns {boolean}
*/ */
export function hasContext(key) { export function hasContext(key) {
const context_map = get_or_init_context_map(); const context_map = get_or_init_context_map('hasContext');
return context_map.has(key); return context_map.has(key);
} }
@ -936,7 +931,7 @@ export function hasContext(key) {
* @returns {T} * @returns {T}
*/ */
export function getAllContexts() { export function getAllContexts() {
const context_map = get_or_init_context_map(); const context_map = get_or_init_context_map('getAllContexts');
if (DEV) { if (DEV) {
// @ts-expect-error // @ts-expect-error
@ -951,16 +946,18 @@ export function getAllContexts() {
return /** @type {T} */ (context_map); return /** @type {T} */ (context_map);
} }
/** @returns {Map<unknown, unknown>} */ /**
function get_or_init_context_map() { * @param {string} name
const component_context = current_component_context; * @returns {Map<unknown, unknown>}
if (component_context === null) { */
throw new Error( function get_or_init_context_map(name) {
'ERR_SVELTE_ORPHAN_CONTEXT' + if (current_component_context === null) {
(DEV ? 'Context can only be used during component initialisation.' : '') e.lifecycle_outside_component(name);
);
} }
return (component_context.c ??= new Map(get_parent_context(component_context) || undefined));
return (current_component_context.c ??= new Map(
get_parent_context(current_component_context) || undefined
));
} }
/** /**

@ -0,0 +1,33 @@
/* This file is generated by scripts/process-messages.js. Do not edit! */
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* Tried to unmount a component that was not mounted
*/
export function lifecycle_double_unmount() {
if (DEV) {
console.warn(`%c[svelte] ${"lifecycle_double_unmount"}\n%c${"Tried to unmount a component that was not mounted"}`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("lifecycle_double_unmount");
}
}
/**
* %parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
* @param {string} parent
* @param {string} child
* @param {string} owner
*/
export function ownership_invalid_binding(parent, child, owner) {
if (DEV) {
console.warn(`%c[svelte] ${"ownership_invalid_binding"}\n%c${`${parent} passed a value to ${child} with \`bind:\`, but the value is owned by ${owner}. Consider creating a binding between ${owner} and ${parent}`}`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("ownership_invalid_binding");
}
}

@ -1,4 +1,5 @@
import { is_void } from '../../compiler/phases/1-parse/utils/names.js'; import { is_void } from '../../compiler/phases/1-parse/utils/names.js';
import * as w from './warnings.js';
const snippet_symbol = Symbol.for('svelte.snippet'); const snippet_symbol = Symbol.for('svelte.snippet');
@ -42,8 +43,7 @@ export function validate_component(component_fn) {
export function validate_void_dynamic_element(tag_fn) { export function validate_void_dynamic_element(tag_fn) {
const tag = tag_fn(); const tag = tag_fn();
if (tag && is_void(tag)) { if (tag && is_void(tag)) {
// eslint-disable-next-line no-console w.dynamic_void_element_content(false, tag);
console.warn(`<svelte:element this="${tag}"> is self-closing and cannot have content.`);
} }
} }

@ -0,0 +1,21 @@
/* This file is generated by scripts/process-messages.js. Do not edit! */
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* `<svelte:element this="%tag%">` is a void element it cannot have content
* @param {boolean} trace
* @param {string} tag
*/
export function dynamic_void_element_content(trace, tag) {
if (DEV) {
console.warn(`%c[svelte] ${"dynamic_void_element_content"}\n%c${`\`<svelte:element this="${tag}">\` is a void element — it cannot have content`}`, bold, normal);
if (trace) console.trace('stack trace');
} else {
// TODO print a link to the documentation
console.warn("dynamic_void_element_content");
}
}

@ -6,5 +6,5 @@ export default test({
unmount(component.l1); unmount(component.l1);
}, },
warnings: ['Tried to unmount a component that was not mounted.'] warnings: ['Tried to unmount a component that was not mounted']
}); });

@ -8,7 +8,7 @@ export default test({
}, },
warnings: [ warnings: [
'Tried to unmount a component that was not mounted.', 'Tried to unmount a component that was not mounted',
'Tried to unmount a component that was not mounted.' 'Tried to unmount a component that was not mounted'
] ]
}); });

@ -7,5 +7,5 @@ export default test({
get props() { get props() {
return { tag: 'br' }; return { tag: 'br' };
}, },
warnings: ['<svelte:element this="br"> is self-closing and cannot have content.'] warnings: ['`<svelte:element this="br">` is a void element — it cannot have content']
}); });

@ -7,5 +7,5 @@ export default test({
get props() { get props() {
return { tag: 'br' }; return { tag: 'br' };
}, },
warnings: ['<svelte:element this="br"> is self-closing and cannot have content.'] warnings: ['`<svelte:element this="br">` is a void element — it cannot have content']
}); });

@ -7,5 +7,5 @@ export default test({
get props() { get props() {
return { tag: 'br' }; return { tag: 'br' };
}, },
warnings: ['<svelte:element this="br"> is self-closing and cannot have content.'] warnings: ['`<svelte:element this="br">` is a void element — it cannot have content']
}); });

@ -8,5 +8,5 @@ export default test({
return { tag: 'br' }; return { tag: 'br' };
}, },
html: '<br>', html: '<br>',
warnings: ['<svelte:element this="br"> is self-closing and cannot have content.'] warnings: ['`<svelte:element this="br">` is a void element — it cannot have content']
}); });

@ -4,5 +4,5 @@ export default test({
compileOptions: { compileOptions: {
dev: true dev: true
}, },
warnings: ['<svelte:element this="input"> is self-closing and cannot have content.'] warnings: ['`<svelte:element this="input">` is a void element — it cannot have content']
}); });

@ -178,7 +178,15 @@ async function run_test_variant(
if (str.slice(0, i).includes('warnings') || config.warnings) { if (str.slice(0, i).includes('warnings') || config.warnings) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn = (...args) => warnings.push(...args); console.warn = (...args) => {
if (args[0].startsWith('%c[svelte]')) {
// TODO convert this to structured data, for more robust comparison?
const message = args[0];
warnings.push(message.slice(message.indexOf('%c', 2) + 2));
} else {
warnings.push(...args);
}
};
} }
} }

@ -327,7 +327,7 @@ describe('signals', () => {
try { try {
flushSync(); flushSync();
} catch (e: any) { } catch (e: any) {
assert.include(e.message, 'ERR_SVELTE_TOO_MANY_UPDATES'); assert.include(e.message, 'effect_update_depth_exceeded');
errored = true; errored = true;
} }
assert.equal(errored, true); assert.equal(errored, true);
@ -348,7 +348,7 @@ describe('signals', () => {
try { try {
flushSync(); flushSync();
} catch (e: any) { } catch (e: any) {
assert.include(e.message, 'ERR_SVELTE_TOO_MANY_UPDATES'); assert.include(e.message, 'effect_update_depth_exceeded');
errored = true; errored = true;
} }
assert.equal(errored, true); assert.equal(errored, true);

Loading…
Cancel
Save