Utility for intercepting and patching Webpack chunks in UserScripts
Dit script moet niet direct worden geïnstalleerd - het is een bibliotheek voor andere scripts om op te nemen met de meta-richtlijn // @require https://update.greasyfork.org/scripts/588910/1887391/Webpack%20Patcher%20Library.js
// ==UserScript==
// @name Webpack Patcher Library
// @namespace webasm
// @version 0.2
// @description Utility for intercepting and patching Webpack chunks in UserScripts
// @author webasm
// @grant unsafeWindow
// ==/UserScript==
(function() {
'use strict';
const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
if (targetWindow.WebpackPatcher) return;
class ModulePatch {
constructor(idOrKeyword, context = null, argNames = ["e", "t", "i"]) {
if (typeof idOrKeyword === 'number') {
this._id = idOrKeyword;
this._keyword = null;
} else {
this._id = null;
this._keyword = Array.isArray(idOrKeyword) ? idOrKeyword : [idOrKeyword];
}
this._context = context;
this._argNames = argNames;
this._isPatched = false;
}
get id() { return this._id; }
get keyword() { return this._keyword; }
get context() { return this._context; }
get isPatched() { return this._isPatched; }
test(moduleId, moduleStr) {
if (this._id !== null) {
return String(moduleId) === String(this._id);
}
if (this._keyword !== null) {
return this._keyword.every(kw => {
if (typeof kw === 'string') {
return moduleStr.includes(kw);
} else if (kw instanceof RegExp) {
return kw.test(moduleStr);
}
return false;
});
}
return false;
}
transformBody(originalBody) {
return originalBody;
}
applyTo(originalModule, matchedId) {
const moduleStr = originalModule.toString();
const match = moduleStr.match(/^(?:async\s+)?(?:function\*?\s*)?(?:\w+\s*)?\((?:[^()]|\([^()]*\))*\)\s*(?:=>)?\s*\{(.*)\}/s);
if (!match) {
console.warn(`[Patch] Regex mismatch for module ${matchedId}. Skipping.`);
return originalModule;
}
const funcBodyStr = match[1];
const modifiedBody = this.transformBody(funcBodyStr);
const errorCatchStart = `try { \n`;
const errorCatchEnd = `\n} catch(err) { console.error("[Patch Error] Module ${matchedId}:", err); }`;
this._isPatched = true;
return new Function(...this._argNames, errorCatchStart + modifiedBody + errorCatchEnd);
}
}
class ModulePatchManager {
constructor() {
this.patches = [];
}
register(patch) {
this.patches.push(patch);
}
unregister(patch) {
this.patches = this.patches.filter(p => p !== patch);
}
processModules(modules, currentContext = null) {
if (!modules) return;
const pendingPatches = [...this.patches];
for (const patch of pendingPatches) {
if (patch.isPatched) continue;
if (patch.context !== null && patch.context !== currentContext) continue;
for (const [moduleId, originalModule] of Object.entries(modules)) {
if (!originalModule) continue;
const moduleStr = originalModule.toString();
if (patch.test(moduleId, moduleStr)) {
try {
const patchedModule = patch.applyTo(originalModule, moduleId);
modules[moduleId] = patchedModule;
console.log(`[PatchManager] Successfully patched module: ${moduleId} (Context: ${currentContext})`);
if (patch.isPatched) {
this.unregister(patch);
}
} catch (error) {
console.error(`[PatchManager] Failed to patch module: ${moduleId} (Context: ${currentContext})`, error);
}
break;
}
}
}
}
}
class WebpackChunkInterceptor {
constructor(patchManager, globalVarName, injectionPayload = null) {
this.patchManager = patchManager;
this.globalVarName = globalVarName;
this.injectionPayload = injectionPayload;
}
_getChunkIdentifier(chunk) {
const originalId = chunk[0][0];
const stack = new Error().stack || "";
const matches = stack.match(/\/([a-zA-Z0-9_.-]+\.js)/g);
if (matches) {
for (const match of matches) {
const filename = match.substring(1);
if (!filename.includes('userscript') &&
!filename.includes('tampermonkey') &&
!filename.includes('violentmonkey')) {
return filename;
}
}
}
return originalId;
}
attach() {
const self = this;
Object.defineProperty(targetWindow, this.globalVarName, {
get() {
return this.__webpackChunkValue;
},
set(value) {
if (Array.isArray(value)) {
value.forEach(chunk => {
const chunkId = self._getChunkIdentifier(chunk);
self.patchManager.processModules(chunk[1], chunkId);
});
const origPush = value.push.bind(value);
value.push = function(...chunks) {
chunks.forEach(chunk => {
const chunkId = self._getChunkIdentifier(chunk);
self.patchManager.processModules(chunk[1], chunkId);
});
return origPush(...chunks);
};
if (self.injectionPayload) {
origPush(self.injectionPayload);
}
}
this.__webpackChunkValue = value;
},
configurable: true
});
console.log(`[Interceptor] Attached webpackChunk Interceptor to ${this.globalVarName}`);
}
}
class WebpackInternalCallInterceptor {
constructor(patchManager, triggerModuleId, targetFilename) {
this.patchManager = patchManager;
this.triggerModuleId = triggerModuleId;
this.targetFilename = targetFilename;
}
attach() {
const self = this;
const origCall = Function.prototype.call;
Function.prototype.call = function(thisArg, ...args) {
const module = args[0];
if (module && String(module.id) === String(self.triggerModuleId)) {
const stack = new Error().stack || "";
if (stack.includes(self.targetFilename)) {
try {
const __webpack_require__ = args[2];
if (__webpack_require__ && __webpack_require__.m) {
self.patchManager.processModules(__webpack_require__.m, self.targetFilename);
}
} finally {
Function.prototype.call = origCall;
console.log(`[Interceptor] Unhooked Webpack Internal Call Interceptor for ${self.targetFilename}`);
}
}
}
return origCall.apply(this, [thisArg, ...args]);
};
console.log(`[Interceptor] Attached Webpack Internal Call Interceptor for ${this.targetFilename} (Trigger ID: ${this.triggerModuleId})`);
}
}
targetWindow.WebpackPatcher = {
ModulePatch,
ModulePatchManager,
WebpackChunkInterceptor,
WebpackInternalCallInterceptor
};
})();