Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 97 additions & 23 deletions packages/browser-extension/src/scripts/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,24 +133,76 @@ export function init(forceStart = false) {
if (!isLeafStackEntry) {
mergedDataStack = Object.assign(mergedDataStack, stackEntry);
} else {
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
leafDataObj[prop] = descriptor.get.call(mergedDataStack);
// 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;
},
);
// First collect own properties of the child (non-getters)
const leafOwnData = {};

// 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 (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--;
Expand Down Expand Up @@ -326,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;
}
Expand All @@ -357,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);
});
Expand Down