From e73d536c1cf878ee52bf794b52853a8667e5ef2f Mon Sep 17 00:00:00 2001 From: David Buhler <13938111+davidjayb@users.noreply.github.com> Date: Mon, 1 Apr 2024 12:26:44 -0600 Subject: [PATCH 1/4] feat: support prop updates on objects with functions (#194) --- src/client/index.js | 62 ++++++++++++++++++++++++++++++--- src/lib/panel/Expandable.svelte | 9 +++-- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/client/index.js b/src/client/index.js index 6ab49f0..f4f7878 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -3,6 +3,8 @@ import { addListener } from './listener.js'; // import { profiler } from './profiler.js'; import { nodes } from './svelte.js'; +const propClones = new Map(); + // @ts-ignore - for the app to call with `eval` window['#SvelteDevTools'] = { /** @@ -12,10 +14,43 @@ window['#SvelteDevTools'] = { */ inject(id, key, value) { const { detail: component } = nodes.map.get(id) || {}; - component && component.$inject_state({ [key]: value }); + + if (component) { + const clone = updateProp(propClones.get(id)[key], value); + + component.$inject_state({ + [key]: clone, + }); + } }, }; +/** + * @param {*} orig + * @param {*} value + * @returns {any} + */ +function updateProp(orig, value, seen = new Map()) { + switch (typeof value) { + case 'object': { + if (value === window || value === null) return null; + if (Array.isArray(value)) return value.map((o, index) => updateProp(orig[index], o, seen)); + if (seen.has(value)) return seen.get(value); + + /** @type {Record} */ + const o = {}; + seen.set(value, o); + for (const [key, v] of Object.entries(value)) { + orig[key] = updateProp(orig[key], v, seen); + } + + return orig; + } + default: + return value; + } +} + const previous = { /** @type {HTMLElement | null} */ target: null, @@ -132,19 +167,38 @@ function serialize(node) { switch (node.type) { case 'component': { const { $$: internal = {} } = node.detail; - const ctx = clone(node.detail.$capture_state?.() || {}); + const nodeState = node.detail.$capture_state?.() || {}; const bindings = Object.values(internal.bound || {}).map( /** @param {Function} f */ (f) => f.name, ); + + /** @type {Record} */ + // clone original prop objects for update + const _propClones = {}; const props = Object.keys(internal.props || {}).flatMap((key) => { - const value = ctx[key]; - delete ctx[key]; // deduplicate for ctx + const prop = nodeState[key]; + + if (prop) { + const prototypeDescriptors = Object.getOwnPropertyDescriptors( + Object.getPrototypeOf(nodeState[key]), + ); + const protoClone = Object.create(null, prototypeDescriptors); + const clone = Object.create(protoClone, Object.getOwnPropertyDescriptors(prop)); + _propClones[key] = clone; + } + + const value = clone(prop); + delete nodeState[key]; // deduplicate for ctx if (value === undefined) return []; const bounded = bindings.some((f) => f.includes(key)); return { key, value, bounded }; }); + propClones.set(res.id, _propClones); + + const ctx = clone(nodeState); + res.detail = { attributes: props, listeners: Object.entries(internal.callbacks || {}).flatMap(([event, value]) => diff --git a/src/lib/panel/Expandable.svelte b/src/lib/panel/Expandable.svelte index 8d27af1..8c2a0ad 100644 --- a/src/lib/panel/Expandable.svelte +++ b/src/lib/panel/Expandable.svelte @@ -24,9 +24,12 @@ case 'number': return value.toString(); case 'object': - return `{${Object.entries(value) - .map(([key, value]) => `"${key}":${key == k ? v : stringify(value)}`) - .join(',')}}`; + // only return updated key + if (k) { + return `{${k}: ${v}}`; + } else { + return `{}`; + } default: // when is this ever the case? return value?.toString() ?? 'undefined'; From 4c2959ad384ae91d377d712ad530149c135747b4 Mon Sep 17 00:00:00 2001 From: Ignatius Bagus Date: Wed, 24 Apr 2024 16:38:43 +0700 Subject: [PATCH 2/4] fine grained clone/serialize --- workspace/extension/src/client/utils.js | 15 +++++++++------ .../extension/src/lib/panel/PropertyList.svelte | 10 ++-------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/workspace/extension/src/client/utils.js b/workspace/extension/src/client/utils.js index 67a3428..9b719c2 100644 --- a/workspace/extension/src/client/utils.js +++ b/workspace/extension/src/client/utils.js @@ -10,14 +10,17 @@ function clone(value, seen = new Map()) { return { __is: 'symbol', name: value.toString() }; case 'object': { if (value === window || value === null) return null; - if (Array.isArray(value)) return value.map((o) => clone(o, seen)); + if (Array.isArray(value)) { + return value.map((o, i) => ({ key: i, value: clone(o, seen) })); + } if (seen.has(value)) return {}; /** @type {Record} */ const o = {}; seen.set(value, o); for (const [key, v] of Object.entries(value)) { - o[key] = clone(v, seen); + const readonly = Object.getOwnPropertyDescriptor(value, key)?.get !== undefined; + o[key] = { key, value: clone(v, seen), readonly }; } return o; } @@ -37,13 +40,13 @@ export function serialize(node) { switch (node.type) { case 'component': { const { $$: internal = {} } = node.detail; - const ctx = clone(node.detail.$capture_state?.() || {}); + const captured = node.detail.$capture_state?.() || {}; const bindings = Object.values(internal.bound || {}).map( /** @param {Function} f */ (f) => f.name, ); const props = Object.keys(internal.props || {}).flatMap((key) => { - const value = ctx[key]; - delete ctx[key]; // deduplicate for ctx + const value = clone(captured[key]); + delete captured[key]; // deduplicate for ctx if (value === undefined) return []; const bounded = bindings.some((f) => f.includes(key)); @@ -55,7 +58,7 @@ export function serialize(node) { listeners: Object.entries(internal.callbacks || {}).flatMap(([event, value]) => value.map(/** @param {Function} f */ (f) => ({ event, handler: f.toString() })), ), - ctx: Object.entries(ctx).map(([key, value]) => ({ key, value })), + ctx: Object.entries(clone(captured)).map(([key, value]) => ({ key, value })), }; break; } diff --git a/workspace/extension/src/lib/panel/PropertyList.svelte b/workspace/extension/src/lib/panel/PropertyList.svelte index e05f678..ea28c55 100644 --- a/workspace/extension/src/lib/panel/PropertyList.svelte +++ b/workspace/extension/src/lib/panel/PropertyList.svelte @@ -72,9 +72,7 @@ Array [{value.length || ''}] {#if value.length && expanded} - {@const entries = value.map((v, i) => ({ key: `${i}`, value: v, readonly }))} - - + {/if} {:else if type === 'object'} {#if value.__is === 'function'} @@ -86,11 +84,7 @@ Object {…} {#if expanded} - {@const entries = Object.entries(value).map(([key, v]) => { - return { key, value: v, readonly }; - })} - - + {/if} {:else} Object { } From faffa188bde184ff575680cadc85aa226c242fd2 Mon Sep 17 00:00:00 2001 From: Ignatius Bagus Date: Wed, 24 Apr 2024 22:05:31 +0700 Subject: [PATCH 3/4] normalize object signature --- workspace/extension/src/client/utils.js | 6 ++---- workspace/extension/src/lib/panel/PropertyList.svelte | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/workspace/extension/src/client/utils.js b/workspace/extension/src/client/utils.js index 9b719c2..741e5f0 100644 --- a/workspace/extension/src/client/utils.js +++ b/workspace/extension/src/client/utils.js @@ -10,9 +10,7 @@ function clone(value, seen = new Map()) { return { __is: 'symbol', name: value.toString() }; case 'object': { if (value === window || value === null) return null; - if (Array.isArray(value)) { - return value.map((o, i) => ({ key: i, value: clone(o, seen) })); - } + if (Array.isArray(value)) return value.map((o) => clone(o, seen)); if (seen.has(value)) return {}; /** @type {Record} */ @@ -58,7 +56,7 @@ export function serialize(node) { listeners: Object.entries(internal.callbacks || {}).flatMap(([event, value]) => value.map(/** @param {Function} f */ (f) => ({ event, handler: f.toString() })), ), - ctx: Object.entries(clone(captured)).map(([key, value]) => ({ key, value })), + ctx: Object.entries(captured).map(([key, v]) => ({ key, value: clone(v) })), }; break; } diff --git a/workspace/extension/src/lib/panel/PropertyList.svelte b/workspace/extension/src/lib/panel/PropertyList.svelte index d288fcb..f9e7c11 100644 --- a/workspace/extension/src/lib/panel/PropertyList.svelte +++ b/workspace/extension/src/lib/panel/PropertyList.svelte @@ -72,7 +72,9 @@ Array [{value.length || ''}] {#if value.length && expanded} - + {@const entries = value.map((v, i) => ({ key: `${i}`, value: v, readonly }))} + + {/if} {:else if type === 'object'} {#if value.__is === 'function'} From 88e1d7df2ca62a9b3bfc1c56c1141a5c9421cdf4 Mon Sep 17 00:00:00 2001 From: Ignatius Bagus Date: Thu, 25 Apr 2024 20:52:44 +0700 Subject: [PATCH 4/4] check for writable --- workspace/extension/src/client/utils.js | 5 ++++- workspace/extension/src/lib/panel/PropertyList.svelte | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/workspace/extension/src/client/utils.js b/workspace/extension/src/client/utils.js index 741e5f0..6137ab2 100644 --- a/workspace/extension/src/client/utils.js +++ b/workspace/extension/src/client/utils.js @@ -16,8 +16,11 @@ function clone(value, seen = new Map()) { /** @type {Record} */ const o = {}; seen.set(value, o); + + const descriptors = Object.getOwnPropertyDescriptors(value); for (const [key, v] of Object.entries(value)) { - const readonly = Object.getOwnPropertyDescriptor(value, key)?.get !== undefined; + const { get, writable } = descriptors[key]; + const readonly = !writable || get !== undefined; o[key] = { key, value: clone(v, seen), readonly }; } return o; diff --git a/workspace/extension/src/lib/panel/PropertyList.svelte b/workspace/extension/src/lib/panel/PropertyList.svelte index 0f827ea..64ed8f1 100644 --- a/workspace/extension/src/lib/panel/PropertyList.svelte +++ b/workspace/extension/src/lib/panel/PropertyList.svelte @@ -85,7 +85,7 @@ {:else if Object.keys(value).length} Object {…} - {#if expanded} + {#if expanded[key]} {/if} {:else}