From 3b1ba02cc36db3d3e66b86a07ab2109c7df44962 Mon Sep 17 00:00:00 2001 From: serhumanos Date: Fri, 12 Jun 2026 16:33:33 -0400 Subject: [PATCH 1/2] fix(backend): correct 'this' context in nested component getters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a getter in a child Alpine.js component references its own properties using 'this', the devtools extension was incorrectly using only the parent's context, causing 'this' to point to the parent instead of the component itself. The fix collects the child's own non-getter properties first, then creates a combined context (parent + child, with child taking precedence) before evaluating getters. This matches Alpine.js's actual runtime behavior. Before: - Getter in child accessing this.childProp → undefined (wrong) - Getter in child accessing this.parentProp → works (correct) After: - Getter in child accessing this.childProp → works (correct) - Getter in child accessing this.parentProp → works (correct) --- .../browser-extension/src/scripts/backend.js | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/browser-extension/src/scripts/backend.js b/packages/browser-extension/src/scripts/backend.js index 1d32271..6665d61 100644 --- a/packages/browser-extension/src/scripts/backend.js +++ b/packages/browser-extension/src/scripts/backend.js @@ -133,6 +133,24 @@ export function init(forceStart = false) { if (!isLeafStackEntry) { mergedDataStack = Object.assign(mergedDataStack, stackEntry); } else { + // First collect own properties of the child (non-getters) + const leafOwnData = {}; + Object.entries(Object.getOwnPropertyDescriptors(stackEntry)).forEach( + ([prop, descriptor]) => { + if (!descriptor.enumerable) { + // magics are non-enumerable + return; + } + if (typeof descriptor.get !== 'function') { + leafOwnData[prop] = descriptor.value; + } + } + ); + + // Full context: parent + child (child overrides parent) + const getterContext = Object.assign({}, mergedDataStack, leafOwnData); + + // Now process all properties with the correct context Object.entries(Object.getOwnPropertyDescriptors(stackEntry)).forEach( ([prop, descriptor]) => { if (!descriptor.enumerable) { @@ -140,8 +158,8 @@ export function init(forceStart = false) { return; } if (typeof descriptor.get === 'function') { - // this is a getter, evaluate with nested context - leafDataObj[prop] = descriptor.get.call(mergedDataStack); + // this is a getter, evaluate with nested context (parent + child) + leafDataObj[prop] = descriptor.get.call(getterContext); // TODO: need to hide the edit button etc, if this // doesn't have a `descriptor.set !== 'function'` function // and/or `!!descriptor.writable` From f6cc5121cc2a2cf3c57f1b04b6efcde937de8404 Mon Sep 17 00:00:00 2001 From: serhumanos Date: Fri, 12 Jun 2026 22:34:27 -0400 Subject: [PATCH 2/2] Fix: Support for classes with inheritance in Alpine DevTools - Modified getAlpineDataInstance() to traverse the prototype chain and capture getters, methods, and properties inherited from classes. - Updated watchComponents() to use getAllEnumerablePropertyNames(), which also traverses the prototype chain, allowing detection of changes in inherited properties. - This enables using Alpine.data() with instances of classes that extend base components (e.g., WizardPasoComponent) while maintaining full visibility in the devtools. --- .../browser-extension/src/scripts/backend.js | 124 +++++++++++++----- 1 file changed, 90 insertions(+), 34 deletions(-) diff --git a/packages/browser-extension/src/scripts/backend.js b/packages/browser-extension/src/scripts/backend.js index 6665d61..d1d23e1 100644 --- a/packages/browser-extension/src/scripts/backend.js +++ b/packages/browser-extension/src/scripts/backend.js @@ -135,40 +135,74 @@ export function init(forceStart = false) { } else { // First collect own properties of the child (non-getters) const leafOwnData = {}; - Object.entries(Object.getOwnPropertyDescriptors(stackEntry)).forEach( - ([prop, descriptor]) => { - if (!descriptor.enumerable) { - // magics are non-enumerable - return; - } - if (typeof descriptor.get !== 'function') { - leafOwnData[prop] = descriptor.value; - } + + // Helper to process descriptors from object and its prototype chain + function processDescriptors(obj, contextObj) { + let currentObj = obj; + const visitedPrototypes = new Set(); + + while (currentObj && currentObj !== Object.prototype && !visitedPrototypes.has(currentObj)) { + visitedPrototypes.add(currentObj); + + Object.entries(Object.getOwnPropertyDescriptors(currentObj)).forEach( + ([prop, descriptor]) => { + if (!descriptor.enumerable) { + // magics are non-enumerable + return; + } + if (typeof descriptor.get !== 'function') { + leafOwnData[prop] = descriptor.value; + } + } + ); + + currentObj = Object.getPrototypeOf(currentObj); } - ); + } + + processDescriptors(stackEntry, stackEntry); // Full context: parent + child (child overrides parent) const getterContext = Object.assign({}, mergedDataStack, leafOwnData); - // Now process all properties with the correct context - Object.entries(Object.getOwnPropertyDescriptors(stackEntry)).forEach( - ([prop, descriptor]) => { - if (!descriptor.enumerable) { - // magics are non-enumerable - return; - } - if (typeof descriptor.get === 'function') { - // this is a getter, evaluate with nested context (parent + child) - leafDataObj[prop] = descriptor.get.call(getterContext); - // TODO: need to hide the edit button etc, if this - // doesn't have a `descriptor.set !== 'function'` function - // and/or `!!descriptor.writable` - } else { - leafDataObj[prop] = descriptor.value; - } - return; - }, - ); + // Now process all properties with the correct context (including prototype chain) + function processDescriptorsWithGetters(obj, contextObj) { + let currentObj = obj; + const visitedPrototypes = new Set(); + + while (currentObj && currentObj !== Object.prototype && !visitedPrototypes.has(currentObj)) { + visitedPrototypes.add(currentObj); + + Object.entries(Object.getOwnPropertyDescriptors(currentObj)).forEach( + ([prop, descriptor]) => { + if (!descriptor.enumerable) { + // magics are non-enumerable + return; + } + if (typeof descriptor.get === 'function') { + // this is a getter, evaluate with nested context (parent + child) + // avoid duplicate evaluation if already processed + if (!(prop in leafDataObj)) { + leafDataObj[prop] = descriptor.get.call(contextObj); + } + // TODO: need to hide the edit button etc, if this + // doesn't have a `descriptor.set !== 'function'` function + // and/or `!!descriptor.writable` + } else { + // only set if not already set + if (!(prop in leafDataObj)) { + leafDataObj[prop] = descriptor.value; + } + } + return; + }, + ); + + currentObj = Object.getPrototypeOf(currentObj); + } + } + + processDescriptorsWithGetters(stackEntry, getterContext); } i--; @@ -344,18 +378,41 @@ export function init(forceStart = false) { if (this.isV3) { const componentData = this.getAlpineDataInstance(rootEl); window?.Alpine?.effect(() => { - Object.keys(componentData).forEach((key) => { + // Helper to get all enumerable property names including from prototype chain + function getAllEnumerablePropertyNames(obj) { + const props = new Set(); + let currentObj = obj; + const visitedPrototypes = new Set(); + + while (currentObj && currentObj !== Object.prototype && !visitedPrototypes.has(currentObj)) { + visitedPrototypes.add(currentObj); + + Object.entries(Object.getOwnPropertyDescriptors(currentObj)).forEach( + ([prop, descriptor]) => { + if (descriptor.enumerable && !prop.startsWith('$') && !prop.startsWith('_x')) { + props.add(prop); + } + } + ); + + currentObj = Object.getPrototypeOf(currentObj); + } + + return Array.from(props); + } + + getAllEnumerablePropertyNames(componentData).forEach((key) => { let recursionDepth = 0; function visit(componentData, key) { recursionDepth += 1; const descriptor = Object.getOwnPropertyDescriptor(componentData, key); - if (descriptor.get && !descriptor.set && !descriptor.value) { + if (descriptor && descriptor.get && !descriptor.set && !descriptor.value) { // this is a getter, we don't need to re-run getters // unless there's a setter for (const stack of rootEl._x_dataStack.slice(1)) { // But ensure Alpine recomputes this effect if any of // the parents change as they could be used in the getter - Object.keys(stack).forEach((k) => { + getAllEnumerablePropertyNames(stack).forEach((k) => { if (recursionDepth >= 10) { return; } @@ -375,8 +432,7 @@ export function init(forceStart = false) { // access the length to be notified of new array items void componentData[key].length; } else { - Object.keys(componentData[key]) - .filter((k) => !k.startsWith('$') && !k.startsWith('_x')) + getAllEnumerablePropertyNames(componentData[key]) .forEach((k) => { visit(componentData[key], k); });