Webpack Patcher Library

Utility for intercepting and patching Webpack chunks in UserScripts

Ce script ne devrait pas être installé directement. C'est une librairie créée pour d'autres scripts. Elle doit être inclus avec la commande // @require https://update.greasyfork.org/scripts/588910/1888149/Webpack%20Patcher%20Library.js

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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) {
      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._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);
          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\S]*)\}$/);

      if (!match) {
        console.warn(`[Patch] Regex mismatch for module ${matchedId}. It might be an unexpected format. Skipping.`);
        return originalModule;
      }

      const argsStr = match[1];
      const funcBodyStr = match[2];
      const args = argsStr.split(',').map(s => s.trim()).filter(Boolean);
      const modifiedBody = this.transformBody(funcBodyStr);

      const errorCatchStart = `try { \n`;
      const errorCatchEnd = `\n} catch(err) { console.error("[Patch Error] Module ${matchedId}:", err); }`;

      try {
        const newFunc = new Function(...args, errorCatchStart + modifiedBody + errorCatchEnd);
        this._isPatched = true;
        return newFunc;
      } catch (err) {
        console.error(`[Patch] Syntax Error while building function for module ${matchedId}. Falling back to original.`, err);
        return originalModule;
      }
    }
  }

  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}`, error);
            }
            break;
          }
        }
      }
    }
  }

  class WebpackChunkInterceptor {
    constructor(patchManager, globalVarName, injectionPayload = null) {
      this.patchManager = patchManager;
      this.globalVarName = globalVarName;
      this.injectionPayload = injectionPayload;
    }

    _getChunkIdentifier(chunk) {
      try {
        return chunk[0][0];
      } catch (e) {
        return "unknown";
      }
    }

    attach() {
      if (this.patchManager.patches.length === 0) return;

      const self = this;
      const globalName = this.globalVarName;

      const hookArray = (arr) => {
        if (!arr || arr.__isHooked) return;
        Object.defineProperty(arr, '__isHooked', { value: true, enumerable: false, writable: true });

        let originalPush = arr.push;

        Object.defineProperty(arr, 'push', {
          get() {
            return function(...args) {
              try {
                args.forEach(chunk => {
                  if (chunk && chunk[1]) {
                    const chunkId = self._getChunkIdentifier(chunk);
                    self.patchManager.processModules(chunk[1], chunkId);
                  }
                });
              } catch (err) {
                console.error("[Interceptor] Error inside push wrapper:", err);
              }
              return originalPush.apply(this, args);
            };
          },
          set(newPush) {
            originalPush = newPush;
          },
          configurable: true,
          enumerable: true
        });

        try {
          arr.forEach(chunk => {
            if (chunk && chunk[1]) {
              const chunkId = self._getChunkIdentifier(chunk);
              self.patchManager.processModules(chunk[1], chunkId);
            }
          });
        } catch (err) {
          console.error("[Interceptor] Error processing existing chunks:", err);
        }

        if (self.injectionPayload) {
          try {
            originalPush.call(arr, self.injectionPayload);
          } catch (err) {
            console.error("[Interceptor] Error injecting payload:", err);
          }
        }
      };

      let existingVal = targetWindow[globalName];
      if (existingVal && Array.isArray(existingVal)) {
        hookArray(existingVal);
      } else if (existingVal === undefined) {
        existingVal = [];
        hookArray(existingVal);
      }

      Object.defineProperty(targetWindow, globalName, {
        get() { return existingVal; },
        set(newVal) {
          existingVal = newVal;
          if (Array.isArray(existingVal)) {
            hookArray(existingVal);
          }
        },
        configurable: true
      });

      console.log(`[Interceptor] Attached webpackChunk Interceptor securely to ${globalName}`);
    }
  }

  class WebpackInternalCallInterceptor {
    constructor(patchManager, triggerModuleId, targetFilename) {
      this.patchManager = patchManager;
      this.triggerModuleId = triggerModuleId;
      this.targetFilename = targetFilename;
    }

    attach() {
      if(this.patchManager.patches.length === 0) return;
      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}`);
    }
  }

  targetWindow.WebpackPatcher = {
    ModulePatch,
    ModulePatchManager,
    WebpackChunkInterceptor,
    WebpackInternalCallInterceptor
  };

})();