refactor: Modernise polyfills and add Promise.withResolvers#1937
refactor: Modernise polyfills and add Promise.withResolvers#19371Code-JS wants to merge 1 commit intoAcode-Foundation:mainfrom
Promise.withResolvers#1937Conversation
Greptile SummaryThis PR refactors However, three critical runtime compatibility issues remain that prevent safe deployment in legacy environments:
Confidence Score: 0/5
Last reviewed commit: 34aa10a |
93df146 to
f6bd10c
Compare
f7bc166 to
f8d08bf
Compare
340b65a to
0e7c792
Compare
Object.hasOwn and ES2024 featuresPromise.withResolvers
3e9e7d0 to
cf37d3f
Compare
| if (argLength >= 1) { | ||
| parent.insertBefore(docFrag, viableNextSibling); | ||
| } |
There was a problem hiding this comment.
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:
| if (argLength >= 1) { | |
| parent.insertBefore(docFrag, viableNextSibling); | |
| } | |
| if (argLength >= 1) { | |
| if (viableNextSibling) { | |
| parent.insertBefore(docFrag, viableNextSibling); | |
| } else { | |
| parent.appendChild(docFrag); | |
| } | |
| } |
| 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; | ||
| } |
There was a problem hiding this comment.
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:
| 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; | |
| } |
| 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)); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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")) { |
There was a problem hiding this comment.
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)
No description provided.