Skip to content

Commit ed7611b

Browse files
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 <[email protected]>
1 parent 836bc60 commit ed7611b

File tree

7 files changed

+84
-4
lines changed

7 files changed

+84
-4
lines changed

.changeset/early-taxis-allow.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 guidance in browser console when logging $state objects

packages/svelte/messages/client-warnings/warnings.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
55
> `%binding%` (%location%) is binding to a non-reactive property
66
7+
## console_log_state
8+
9+
> Your `console.%method%` contained `$state` proxies. Consider using `$inspect(...)` or `$state.snapshot(...)` instead
10+
11+
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.
12+
13+
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.
14+
715
## event_handler_invalid
816

917
> %handler% should be a function. Did you mean to %suggestion%?

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @import { CallExpression, Expression } from 'estree' */
22
/** @import { Context } from '../types' */
3-
import { is_ignored } from '../../../../state.js';
3+
import { dev, is_ignored } from '../../../../state.js';
44
import * as b from '../../../../utils/builders.js';
55
import { get_rune } from '../../../scope.js';
66
import { transform_inspect_rune } from '../../utils.js';
@@ -35,5 +35,28 @@ export function CallExpression(node, context) {
3535
return transform_inspect_rune(node, context);
3636
}
3737

38+
if (
39+
dev &&
40+
node.callee.type === 'MemberExpression' &&
41+
node.callee.object.type === 'Identifier' &&
42+
node.callee.object.name === 'console' &&
43+
context.state.scope.get('console') === null &&
44+
node.callee.property.type === 'Identifier' &&
45+
['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes(
46+
node.callee.property.name
47+
)
48+
) {
49+
return b.call(
50+
node.callee,
51+
b.spread(
52+
b.call(
53+
'$.log_if_contains_state',
54+
b.literal(node.callee.property.name),
55+
.../** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg)))
56+
)
57+
)
58+
);
59+
}
60+
3861
context.next();
3962
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { STATE_SYMBOL } from '../constants.js';
2+
import { snapshot } from '../../shared/clone.js';
3+
import * as w from '../warnings.js';
4+
5+
/**
6+
* @param {string} method
7+
* @param {...any} objects
8+
*/
9+
export function log_if_contains_state(method, ...objects) {
10+
let has_state = false;
11+
const transformed = [];
12+
13+
for (const obj of objects) {
14+
if (obj && typeof obj === 'object' && STATE_SYMBOL in obj) {
15+
transformed.push(snapshot(obj, true));
16+
has_state = true;
17+
} else {
18+
transformed.push(obj);
19+
}
20+
}
21+
22+
if (has_state) {
23+
w.console_log_state(method);
24+
25+
// eslint-disable-next-line no-console
26+
console.log('%c[snapshot]', 'color: grey', ...transformed);
27+
}
28+
29+
return objects;
30+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,4 @@ export {
164164
validate_void_dynamic_element
165165
} from '../shared/validate.js';
166166
export { strict_equals, equals } from './dev/equality.js';
167+
export { log_if_contains_state } from './dev/console-log.js';

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ export function binding_property_non_reactive(binding, location) {
1919
}
2020
}
2121

22+
/**
23+
* Your `console.%method%` contained `$state` proxies. Consider using `$inspect(...)` or `$state.snapshot(...)` instead
24+
* @param {string} method
25+
*/
26+
export function console_log_state(method) {
27+
if (DEV) {
28+
console.warn(`%c[svelte] console_log_state\n%cYour \`console.${method}\` contained \`$state\` proxies. Consider using \`$inspect(...)\` or \`$state.snapshot(...)\` instead`, bold, normal);
29+
} else {
30+
// TODO print a link to the documentation
31+
console.warn("console_log_state");
32+
}
33+
}
34+
2235
/**
2336
* %handler% should be a function. Did you mean to %suggestion%?
2437
* @param {string} handler

packages/svelte/src/internal/shared/clone.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ const empty = [];
1818
* @returns {Snapshot<T>}
1919
*/
2020
export function snapshot(value, skip_warning = false) {
21-
if (DEV) {
21+
if (DEV && !skip_warning) {
2222
/** @type {string[]} */
2323
const paths = [];
2424

2525
const copy = clone(value, new Map(), '', paths);
26-
if (paths.length === 1 && paths[0] === '' && !skip_warning) {
26+
if (paths.length === 1 && paths[0] === '') {
2727
// value could not be cloned
2828
w.state_snapshot_uncloneable();
29-
} else if (paths.length > 0 && !skip_warning) {
29+
} else if (paths.length > 0) {
3030
// some properties could not be cloned
3131
const slice = paths.length > 10 ? paths.slice(0, 7) : paths.slice(0, 10);
3232
const excess = paths.length - slice.length;

0 commit comments

Comments
 (0)