fix: resolve React DOM insertBefore reconciliation crash on launcher start

This commit is contained in:
tomasalias 2026-10-06 19:27:02 +02:00
parent 945302a3cb
commit 4e19e6f54e
2 changed files with 67 additions and 108 deletions

View file

@ -28,7 +28,6 @@
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
const AdBlocker = { const AdBlocker = {
_observer: null,
_style: null, _style: null,
init() { init() {
@ -44,32 +43,26 @@
.subscribe-link, .subscribe-link,
.subscribe-link-small-resolution, .subscribe-link-small-resolution,
.subscribe-link-large-resolution, .subscribe-link-large-resolution,
.promo-banner { .promo-banner,
owadview,
[class*="ad-container"],
[class*="ad-banner"],
[class*="AdView"],
[class*="high-impact"],
[data-ad],
[data-ad-slot] {
display: none !important; display: none !important;
width: 0 !important; width: 0 !important;
height: 0 !important; height: 0 !important;
min-width: 0 !important; min-width: 0 !important;
max-width: 0 !important; max-width: 0 !important;
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important; overflow: hidden !important;
pointer-events: none !important; pointer-events: none !important;
position: absolute !important; position: absolute !important;
visibility: hidden !important; visibility: hidden !important;
} opacity: 0 !important;
owadview {
display: none !important;
width: 0 !important;
height: 0 !important;
position: absolute !important;
visibility: hidden !important;
}
[class*="ad-container"],
[class*="ad-banner"],
[class*="high-impact-ad"],
[data-ad],
[data-ad-slot] {
display: none !important;
} }
[style*="grid-template"][style*="ads"] { [style*="grid-template"][style*="ads"] {
@ -79,80 +72,28 @@
`; `;
(document.head || document.documentElement).appendChild(this._style); (document.head || document.documentElement).appendChild(this._style);
this._observer = new MutationObserver((mutations) => { // Safe cleanup of ad text wrappers without removing DOM nodes (avoids breaking React Fiber)
for (const mutation of mutations) { setTimeout(() => this._sweepAds(), 1500);
for (const node of mutation.addedNodes) { setTimeout(() => this._sweepAds(), 4000);
if (node.nodeType !== Node.ELEMENT_NODE) continue;
this._removeAds(node);
}
}
});
this._observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
setTimeout(() => this._sweepAds(), 1000);
setTimeout(() => this._sweepAds(), 3000);
setTimeout(() => this._sweepAds(), 6000);
},
_removeAds(node) {
if (node.tagName?.toLowerCase() === 'owadview') {
const parent = node.parentElement;
node.remove();
if (parent && parent.children.length === 0) {
parent.style.display = 'none';
}
return;
}
const classes = node.className?.toString() || '';
if (/\b(ad-container|ad-banner|ads-wrapper|AdView|high-impact|display-ad)\b/i.test(classes)) {
node.remove();
return;
}
const adChildren = node.querySelectorAll?.('owadview, [class*="ad-container"], [class*="AdView"], [class*="high-impact"]');
if (adChildren) {
for (const child of adChildren) {
child.remove();
}
}
}, },
_sweepAds() { _sweepAds() {
const adSelectors = [ try {
'.curseforge-ad', const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT, null);
'.ad-inner', while (walker.nextNode()) {
'.overwolf-ad', const text = walker.currentNode.textContent?.trim();
'.affiliate-banner', if (text && /^Ads support authors/i.test(text)) {
'.subscribe-link', let parent = walker.currentNode.parentElement;
'.subscribe-link-small-resolution', while (parent && parent !== document.body) {
'.subscribe-link-large-resolution', if (parent.children.length <= 2) {
'.promo-banner', parent.style.display = 'none';
'owadview', break;
]; }
const els = document.querySelectorAll(adSelectors.join(', ')); parent = parent.parentElement;
for (const el of els) {
el.remove();
}
const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT, null);
while (walker.nextNode()) {
const text = walker.currentNode.textContent?.trim();
if (text && /^Ads support authors/i.test(text)) {
let parent = walker.currentNode.parentElement;
while (parent && parent !== document.body) {
if (parent.children.length <= 2) {
parent.style.display = 'none';
break;
} }
parent = parent.parentElement;
} }
} }
} } catch {}
}, },
}; };
@ -515,6 +456,8 @@
try { try {
await renderCustomThemesSection(view); await renderCustomThemesSection(view);
} catch (err) {
console.warn('[BetterCF] Themes section rendering failed:', err);
} finally { } finally {
isInjecting = false; isInjecting = false;
} }
@ -592,8 +535,12 @@
lastRow.after(section); lastRow.after(section);
} else { } else {
const upsell = view.querySelector('.upsell') || view.querySelector('[class*="upsell"]'); const upsell = view.querySelector('.upsell') || view.querySelector('[class*="upsell"]');
if (upsell) { if (upsell && upsell.parentNode) {
view.insertBefore(section, upsell); try {
upsell.before(section);
} catch {
view.appendChild(section);
}
} else { } else {
view.appendChild(section); view.appendChild(section);
} }

View file

@ -14,6 +14,7 @@ import hashlib
import winreg import winreg
import time import time
import subprocess import subprocess
import re
from pathlib import Path from pathlib import Path
CREATE_NO_WINDOW = 0x08000000 if os.name == "nt" else 0 CREATE_NO_WINDOW = 0x08000000 if os.name == "nt" else 0
@ -401,35 +402,46 @@ def patch_curseforge(log_callback=print) -> bool:
return False return False
try: try:
# 1. Zero-Ad & Themes Patch in desktop.js # 1. Themes Patch in desktop.js (Ads are fully handled by desktop.css & network blocking)
desktop_js = temp_extract / "dist" / "desktop" / "desktop.js" desktop_js = temp_extract / "dist" / "desktop" / "desktop.js"
if desktop_js.exists(): if desktop_js.exists():
content = desktop_js.read_text(encoding="utf-8") content = desktop_js.read_text(encoding="utf-8")
# Clean up any legacy or duplicate __orig_eV replacements # Restore clean eV component if previously patched (in modern CurseForge, eV is the modpack card author component, not ads)
if "/*BCF_AD_OFF*/eV=e=>{return null;let" in content:
content = content.replace("/*BCF_AD_OFF*/eV=e=>{return null;let", "eV=e=>{let")
log_callback("[+] Restored intact modpack card component (eV)")
while "let __orig_eV=e=>null;" in content: while "let __orig_eV=e=>null;" in content:
content = content.replace("let __orig_eV=e=>null;", "") content = content.replace("let __orig_eV=e=>null;", "")
if "eV=e=>null;let __orig_eV=e=>{" in content: if "eV=e=>null;let __orig_eV=e=>{" in content:
content = content.replace("eV=e=>null;let __orig_eV=e=>{", "eV=e=>{") content = content.replace("eV=e=>null;let __orig_eV=e=>{", "eV=e=>{")
# Idempotently disable React ad component (eV -> return null) # Unlock all native premium themes (Settings & Popup routes) across versions
if "/*BCF_AD_OFF*/" not in content and "eV=e=>{let" in content: content, c1 = re.subn(
content = content.replace("eV=e=>{let", "/*BCF_AD_OFF*/eV=e=>{return null;let", 1) r'([a-zA-Z0-9_$]+)=e=>!!e\.forceUnlock\|\|!e\.isPremium\|\|[^,;]+',
log_callback("[+] Disabled React ad component (eV -> return null)") r'\1=e=>!0',
if "showSwapAdsButton:T" in content: content
content = content.replace("showSwapAdsButton:T", "showSwapAdsButton:!1") )
log_callback("[+] Disabled swap ads button") if c1 > 0:
if "R=e=>!!e.forceUnlock||!e.isPremium||!!M" in content: log_callback(f"[+] Unlocked native premium themes ({c1} route checks)")
content = content.replace("R=e=>!!e.forceUnlock||!e.isPremium||!!M", "R=e=>!0")
log_callback("[+] Unlocked all native premium themes (Settings route)") # Unlock premium theme state validation
if "P=e=>!!e.forceUnlock||!e.isPremium||f" in content: content, c2 = re.subn(
content = content.replace("P=e=>!!e.forceUnlock||!e.isPremium||f", "P=e=>!0") r'return!r\?\.forceUnlock&&r\?\.isPremium&&!t\?[a-zA-Z0-9_$.]+:n',
log_callback("[+] Unlocked all native premium themes (Popup route)") r'return n',
if "return!r?.forceUnlock&&r?.isPremium&&!t?u.Sx.Classic:n" in content: content
content = content.replace("return!r?.forceUnlock&&r?.isPremium&&!t?u.Sx.Classic:n", "return n") )
if c2 > 0:
log_callback("[+] Unlocked premium theme state validation") log_callback("[+] Unlocked premium theme state validation")
if "function iy(e){return e.subscription.plan?.active}" in content:
content = content.replace("function iy(e){return e.subscription.plan?.active}", "function iy(e){return !0}") # Unlock subscription plan status for themes
log_callback("[+] Unlocked subscription plan status for themes") content, c3 = re.subn(
r'function ([a-zA-Z0-9_$]+)\(e\)\{return e\.subscription\.plan\?\.active\}',
r'function \1(e){return !0}',
content
)
if c3 > 0:
log_callback(f"[+] Unlocked subscription plan status for themes ({c3} checks)")
desktop_js.write_text(content, encoding="utf-8") desktop_js.write_text(content, encoding="utf-8")
# 2. Zero-Ad & Settings Themes CSS in desktop.css # 2. Zero-Ad & Settings Themes CSS in desktop.css