Skip to content

Commit

Permalink
feat: provide guidance in browser console when logging $state objec…
Browse files Browse the repository at this point in the history
…ts (#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>
  • Loading branch information
dummdidumm and Rich-Harris authored Sep 16, 2024
1 parent 836bc60 commit ed7611b
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/early-taxis-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: provide guidance in browser console when logging $state objects
8 changes: 8 additions & 0 deletions packages/svelte/messages/client-warnings/warnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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%?
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
}
30 changes: 30 additions & 0 deletions packages/svelte/src/internal/client/dev/console-log.js
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
13 changes: 13 additions & 0 deletions packages/svelte/src/internal/client/warnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/svelte/src/internal/shared/clone.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit ed7611b

Please sign in to comment.