Skip to content

refactor: Modernise polyfills and add Promise.withResolvers#1937

Open
1Code-JS wants to merge 1 commit intoAcode-Foundation:mainfrom
1Code-JS:polyfill
Open

refactor: Modernise polyfills and add Promise.withResolvers#1937
1Code-JS wants to merge 1 commit intoAcode-Foundation:mainfrom
1Code-JS:polyfill

Conversation

@1Code-JS
Copy link
Contributor

@1Code-JS 1Code-JS commented Mar 9, 2026

No description provided.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR refactors src/lib/polyfill.js to modernise polyfills and add Promise.withResolvers. The structural improvements are positive: all methods are now non-enumerable, ownerDocument is correctly used instead of the global document (fixing cross-document node creation), argument validation is separated from DOM mutation, and the toggleAttribute force parameter is properly implemented.

However, three critical runtime compatibility issues remain that prevent safe deployment in legacy environments:

  • Line 129: replaceWith calls this.remove(), which is absent in IE 11 and earlier. The polyfill targets these exact legacy browsers (those lacking native replaceWith). Use parent.removeChild(this) instead.
  • Line 131: When viableNextSibling is null, the code calls insertBefore(docFrag, null), which throws TypeError in IE 8 and earlier. Branch explicitly on whether viableNextSibling is present.
  • Line 193: Object.hasOwn(Promise, "withResolvers") assumes Promise is defined globally, but throws ReferenceError in very old environments without Promise support. Guard with typeof Promise !== "undefined".

Confidence Score: 0/5

  • This PR introduces three blocking runtime errors that prevent safe use in the legacy environments it targets.
  • The refactoring addresses several real issues from earlier review cycles (incorrect ownerDocument usage, missing force-parameter support in toggleAttribute), and the structural improvements are solid. However, the implementation has three concrete bugs that prevent safe deployment: (1) this.remove() is called but unsupported in IE 11 and earlier, which is a primary target for this polyfill; (2) insertBefore(docFrag, null) fails in IE 8 due to non-standard error handling; (3) the Promise.withResolvers polyfill guard fails to check if Promise exists before referencing it globally. All three are verifiable runtime errors in their target environments and must be fixed before merging.
  • src/lib/polyfill.js — specifically lines 129 (this.remove), 131 (insertBefore null handling), and 193 (Promise guard)

Last reviewed commit: 34aa10a

@1Code-JS 1Code-JS force-pushed the polyfill branch 2 times, most recently from 93df146 to f6bd10c Compare March 9, 2026 21:20
@1Code-JS 1Code-JS marked this pull request as draft March 9, 2026 21:37
@1Code-JS 1Code-JS force-pushed the polyfill branch 2 times, most recently from f7bc166 to f8d08bf Compare March 9, 2026 22:49
@1Code-JS 1Code-JS marked this pull request as ready for review March 9, 2026 22:52
@1Code-JS 1Code-JS marked this pull request as draft March 9, 2026 23:00
@1Code-JS 1Code-JS force-pushed the polyfill branch 4 times, most recently from 340b65a to 0e7c792 Compare March 10, 2026 00:24
@1Code-JS 1Code-JS marked this pull request as ready for review March 10, 2026 00:26
@1Code-JS 1Code-JS changed the title refactor: Modernise polyfills with Object.hasOwn and ES2024 features refactor: Modernise polyfills and add Promise.withResolvers Mar 10, 2026
@1Code-JS 1Code-JS force-pushed the polyfill branch 2 times, most recently from 3e9e7d0 to cf37d3f Compare March 10, 2026 03:06
Comment on lines +128 to 130
if (argLength >= 1) {
parent.insertBefore(docFrag, viableNextSibling);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

insertBefore(docFrag, null) compatibility with older browsers

When viableNextSibling is null, parent.insertBefore(docFrag, viableNextSibling) is called. Per the DOM spec, insertBefore(node, null) is equivalent to appendChild(node), and modern browsers implement this correctly. However, some older browsers (notably IE ≤ 8) throw a TypeError when the second argument of insertBefore is null rather than a Node.

For full compatibility with legacy environments, the code should branch explicitly:

Suggested change
if (argLength >= 1) {
parent.insertBefore(docFrag, viableNextSibling);
}
if (argLength >= 1) {
if (viableNextSibling) {
parent.insertBefore(docFrag, viableNextSibling);
} else {
parent.appendChild(docFrag);
}
}

Comment on lines +191 to +215
if (!Object.hasOwn(Promise, "withResolvers")) {
Object.defineProperty(Promise, "withResolvers", {
configurable: true,
enumerable: false,
writable: true,
value: function withResolvers() {
var resolve, reject;
var promise = new this(function (_resolve, _reject) {
resolve = _resolve;
reject = _reject;
});
if (typeof resolve !== "function" || typeof reject !== "function") {
throw new TypeError(
"Promise resolve or reject function is not callable",
);
}
return {
promise: promise,
resolve: resolve,
reject: reject,
};
},
});
Promise.withResolvers.prototype = null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing guard for Promise in very old environments

Line 191 evaluates Object.hasOwn(Promise, "withResolvers") without first checking whether Promise itself is defined. In very old environments where Promise does not exist as a global, this will throw a ReferenceError: Promise is not defined before the polyfill can even install.

Add a typeof Promise !== "undefined" guard:

Suggested change
if (!Object.hasOwn(Promise, "withResolvers")) {
Object.defineProperty(Promise, "withResolvers", {
configurable: true,
enumerable: false,
writable: true,
value: function withResolvers() {
var resolve, reject;
var promise = new this(function (_resolve, _reject) {
resolve = _resolve;
reject = _reject;
});
if (typeof resolve !== "function" || typeof reject !== "function") {
throw new TypeError(
"Promise resolve or reject function is not callable",
);
}
return {
promise: promise,
resolve: resolve,
reject: reject,
};
},
});
Promise.withResolvers.prototype = null;
}
if (typeof Promise !== "undefined" && !Object.hasOwn(Promise, "withResolvers")) {
Object.defineProperty(Promise, "withResolvers", {
configurable: true,
enumerable: false,
writable: true,
value: function withResolvers() {
var resolve, reject;
var promise = new this(function (_resolve, _reject) {
resolve = _resolve;
reject = _reject;
});
if (typeof resolve !== "function" || typeof reject !== "function") {
throw new TypeError(
"Promise resolve or reject function is not callable",
);
}
return {
promise: promise,
resolve: resolve,
reject: reject,
};
},
});
Promise.withResolvers.prototype = null;
}

Comment on lines +108 to +116
do {
if (ancestor !== currentNode) continue;
throw new DOMException(
"Failed to execute 'replaceWith' on '" +
className +
"': The new child element contains the parent.",
"HierarchyRequestError",
);
} while ((ancestor = ancestor.parentNode));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continue in do-while skips to the condition, not the loop body

Using continue inside a do-while loop is correct but subtle: it jumps directly to the while-condition (ancestor = ancestor.parentNode), which is what advances the walk up the tree. This is easy to misread as a regular continue-to-top-of-loop.

More importantly, there is a logical gap: currentNode === parent will correctly throw (since parent is checked in the first iteration), but currentNode === this will not throw — the walk starts from parent and traverses upward through parent.parentNode etc., never visiting descendants of parent. While this being in the argument list is handled separately via isItselfInFragment, no check prevents the user from passing currentNode that is a sibling or another descendant of parent, yet is itself the node hosting this — the ancestor check exclusively guards against cycle-creating insertions, not against passing this's own children. This is actually spec-correct behaviour, but the continue-based idiom is worth documenting with a comment to avoid future regressions:

// Walk UP from parent; if we reach currentNode, it is
// an ancestor of parent — inserting it would create a cycle.
var ancestor = parent;
do {
    if (ancestor === currentNode) {
        throw new DOMException(
            "Failed to execute 'replaceWith' on '" +
                className +
                "': The new child element contains the parent.",
            "HierarchyRequestError",
        );
    }
} while ((ancestor = ancestor.parentNode));

The continue-based inversion makes the throw condition less obvious than a positive equality check.

}
docFrag.appendChild(currentNode);
}
if (!isItselfInFragment) this.remove();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.remove() is unsupported in IE 11 and earlier. This polyfill targets legacy environments that lack native replaceWith, which includes IE 11. The universally-supported alternative is parent.removeChild(this):

if (!isItselfInFragment) parent.removeChild(this);

}
if (!isItselfInFragment) this.remove();
if (argLength >= 1) {
parent.insertBefore(docFrag, viableNextSibling);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In IE 8 and earlier, insertBefore with null as the second argument throws TypeError, even though the DOM spec treats it as equivalent to appendChild. For full legacy compatibility, branch explicitly when viableNextSibling is null:

if (viableNextSibling) {
    parent.insertBefore(docFrag, viableNextSibling);
} else {
    parent.appendChild(docFrag);
}


// polyfill for Promise.withResolvers

if (!Object.hasOwn(Promise, "withResolvers")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.hasOwn(Promise, "withResolvers") is evaluated unconditionally at module scope. In very old environments where the global Promise is not defined, this throws ReferenceError before the polyfill can run. Add a guard:

if (typeof Promise !== "undefined" && !Object.hasOwn(Promise, "withResolvers")) {

- Implement spec-compliant `replaceWith` with hierarchy checks.
- Add `Object.hasOwn` and [`Promise.withResolvers`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers) polyfills.
- Update `toggleAttribute` to support the `force` parameter.
- Optimise `prepend` performance by using standard loops.
- Ensure all polyfills are non-enumerable and have null prototypes.

(AI generated commit message)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant