feat: provide guidance in browser console when logging `$state` objects (#13142)

* feat: provide guidance in browser console when logging `$state` objects

Wrap console.log/warn/error statements in DEV mode with a check whether or not they contain state objects. Closes #13123

This is an alternative or enhancement to #13070. Alternative if we deem it the better solution. Enhancement because it's not as robust as a custom formatter: We only check the top level of each entry (though we could maybe traverse a few levels), and if you're logging class instances, snapshot currently stops at the boundaries there and so you don't get snapshotted values for these (arguably this is a more general problem of $inspect and $state.snapshot), whereas with custom formatter it doesn't matter at which level you come across it.

* lint

* use normal warning mechanism, so we can link to docs etc

* add a few more methods

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
better-docs-for-css-injected
Simon H 12 months ago committed by GitHub
parent 836bc605f4
commit ed7611b163
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: provide guidance in browser console when logging $state objects

@ -4,6 +4,14 @@
> `%binding%` (%location%) is binding to a non-reactive property
## console_log_state
> Your `console.%method%` contained `$state` proxies. Consider using `$inspect(...)` or `$state.snapshot(...)` instead
When logging a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), browser devtools will log the proxy itself rather than the value it represents. In the case of Svelte, the 'target' of a `$state` proxy might not resemble its current value, which can be confusing.
The easiest way to log a value as it changes over time is to use the [`$inspect`](https://svelte-5-preview.vercel.app/docs/runes#$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](https://svelte-5-preview.vercel.app/docs/runes#$state-snapshot) to take a snapshot of the current value.
## event_handler_invalid
> %handler% should be a function. Did you mean to %suggestion%?

@ -1,6 +1,6 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import { is_ignored } from '../../../../state.js';
import { dev, is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
@ -35,5 +35,28 @@ export function CallExpression(node, context) {
return transform_inspect_rune(node, context);
}
if (
dev &&
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'console' &&
context.state.scope.get('console') === null &&
node.callee.property.type === 'Identifier' &&
['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes(
node.callee.property.name
)
) {
return b.call(
node.callee,
b.spread(
b.call(
'$.log_if_contains_state',
b.literal(node.callee.property.name),
.../** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg)))
)
)
);
}
context.next();
}

@ -0,0 +1,30 @@
import { STATE_SYMBOL } from '../constants.js';
import { snapshot } from '../../shared/clone.js';
import * as w from '../warnings.js';
/**
* @param {string} method
* @param {...any} objects
*/
export function log_if_contains_state(method, ...objects) {
let has_state = false;
const transformed = [];
for (const obj of objects) {
if (obj && typeof obj === 'object' && STATE_SYMBOL in obj) {
transformed.push(snapshot(obj, true));
has_state = true;
} else {
transformed.push(obj);
}
}
if (has_state) {
w.console_log_state(method);
// eslint-disable-next-line no-console
console.log('%c[snapshot]', 'color: grey', ...transformed);
}
return objects;
}

@ -164,3 +164,4 @@ export {
validate_void_dynamic_element
} from '../shared/validate.js';
export { strict_equals, equals } from './dev/equality.js';
export { log_if_contains_state } from './dev/console-log.js';

@ -19,6 +19,19 @@ export function binding_property_non_reactive(binding, location) {
}
}
/**
* Your `console.%method%` contained `$state` proxies. Consider using `$inspect(...)` or `$state.snapshot(...)` instead
* @param {string} method
*/
export function console_log_state(method) {
if (DEV) {
console.warn(`%c[svelte] console_log_state\n%cYour \`console.${method}\` contained \`$state\` proxies. Consider using \`$inspect(...)\` or \`$state.snapshot(...)\` instead`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("console_log_state");
}
}
/**
* %handler% should be a function. Did you mean to %suggestion%?
* @param {string} handler

@ -18,15 +18,15 @@ const empty = [];
* @returns {Snapshot<T>}
*/
export function snapshot(value, skip_warning = false) {
if (DEV) {
if (DEV && !skip_warning) {
/** @type {string[]} */
const paths = [];
const copy = clone(value, new Map(), '', paths);
if (paths.length === 1 && paths[0] === '' && !skip_warning) {
if (paths.length === 1 && paths[0] === '') {
// value could not be cloned
w.state_snapshot_uncloneable();
} else if (paths.length > 0 && !skip_warning) {
} else if (paths.length > 0) {
// some properties could not be cloned
const slice = paths.length > 10 ? paths.slice(0, 7) : paths.slice(0, 10);
const excess = paths.length - slice.length;

Loading…
Cancel
Save