Skip to content

Commit

Permalink
Refactor whew, add autodetection support
Browse files Browse the repository at this point in the history
  • Loading branch information
adryd325 committed Nov 30, 2023
1 parent 45b924b commit 1d1c8c3
Show file tree
Hide file tree
Showing 8 changed files with 363 additions and 265 deletions.
211 changes: 211 additions & 0 deletions src/Patcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import matchModule from "./matchModule";
import { getWpToolsFunc } from "./wpTools";

export default class Patcher {
constructor(config) {
this.name = config.name;
this.chunkObject = config.chunkObject;
this.webpackVersion = config.webpackVersion;
this.inspectAll = config.inspectAll;

this.modules = new Set(config.modules ?? []);
this.patches = new Set(config.patches ?? []);

// Validation
if (typeof this.webpackVersion == "number") {
this.webpackVersion = this.webpackVersion.toString();
}

// Populate patches to apply and modules to inject
this.patchesToApply = new Set();
if (this.patches) {
for (const patch of this.patches) {
this.patchesToApply.add(patch);
}
}

this.modulesToInject = new Set();
if (this.modules) {
for (const module of this.modules) {
if (module.needs != undefined && module.needs instanceof Array) {
module.needs = new Set(module.needs);
}
this.modulesToInject.add(module);
}
}

if (config.injectWpTools) {
this.modulesToInject.add({
name: "wpTools",
// This is sorta a scope hack.
// If we rewrap this function, it will lose its scope (in this case the match module import and the chunk object name)
run: getWpToolsFunc(this.chunkObject),
entry: true,
});
}
}

run() {
if (this.webpackVersion == "4" || this.webpackVersion == "5") {
this._interceptWebpackModern();
} else {
this._interceptWebpackLegacy;
}
}

_interceptWebpackModern() {
// This is necesary since some sites (twitter) define the chunk object earlier
let realChunkObject = window[this.chunkObject];
const patcher = this;

Object.defineProperty(window, this.chunkObject, {
set: function set(value) {
realChunkObject = value;
// Don't infinitely re-wrap .push()
// Every webpack chunk reassigns the chunk array, triggering the setter every time
// `(self.webpackChunk = self.webpackChunk || [])`
if (!value.push.__wpt_injected) {
realChunkObject = value;
const realPush = value.push;

value.push = function (chunk) {
// This is necesary because webpack will re-wrap the .push function
// Without this check, we'll patch modules multiple times
if (!chunk.__wpt_processed) {
chunk.__wpt_processed = true;
patcher._patchModules(chunk[1]);
patcher._injectModules(chunk);
}
return realPush.apply(this, arguments);
};

value.push.__wpt_injected = true;
if (realPush == Array.prototype.push) {
console.log("[wpTools] Injected " + patcher._chunkObject + " (before webpack runtime)");
} else {
console.log("[wpTools] Injected " + patcher._chunkObject + " (at webpack runtime)");
}
}
},
get: function get() {
return realChunkObject;
},
configurable: true,
});
}

_interceptWebpackLegacy() {}

_patchModules(modules) {
for (const id in modules) {
if (modules[id].__wpt_processed) {
continue;
}
let funcStr = Function.prototype.toString.apply(modules[id]);

const matchingPatches = [];
for (const patch of this.patchesToApply) {
if (matchModule(funcStr, patch.find)) {
matchingPatches.push(patch);
this.patchesToApply.delete(patch);
}
}

for (const patch of matchingPatches) {
funcStr = funcStr.replace(patch.replace.match, patch.replace.replacement);
}

if (matchingPatches.length > 0 || this.inspectAll) {
let debugString = "";
if (matchingPatches.length > 0) {
debugString += "Patched by: " + matchingPatches.map((patch) => patch.name).join(", ");
}

modules[id] = new Function(
"module",
"exports",
"webpackRequire",
`(${funcStr}).apply(this, arguments)\n// ${debugString}\n//# sourceURL=${this.chunkObject}-Module-${id}`,
);
modules[id].__wpt_patched = true;
}

modules[id].__wpt_funcStr = funcStr;
modules[id].__wpt_processed = true;
}
}

_injectModules(chunk) {
const readyModules = new Set();

for (const moduleToInject of this.modulesToInject) {
if (moduleToInject?.needs?.size > 0) {
for (const need of moduleToInject.needs) {
for (const wpModule of Object.entries(chunk[1])) {
// match { moduleId: "id" } as well as strings and regex
if ((need?.moduleId && wpModule[0] == need.moduleId) || matchModule(wpModule[1].__wpt_funcStr, need)) {
moduleToInject.needs.delete(need);
if (moduleToInject.needs.size == 0) {
readyModules.add(moduleToInject);
}
break;
}
}
}
} else {
readyModules.add(moduleToInject);
}
}

if (readyModules.size > 0) {
const injectModules = {};
const injectEntries = [];

for (const readyModule of readyModules) {
this.modulesToInject.delete(readyModule);
injectModules[readyModule.name] = readyModule.run;
if (readyModule.entry) {
injectEntries.push(readyModule.name);
}
}

// Convert array to object for named modules
if (chunk[1] instanceof Array) {
const origChunkArray = chunk[1];
chunk[1] = {};
origChunkArray.forEach((module, index) => {
chunk[1][index] = module;
});
}

// merge our modules with original modules
chunk[1] = Object.assign(chunk[1], injectModules);

if (injectEntries.length > 0) {
switch (this.webpackVersion) {
case "5":
if (chunk[2]) {
const originalEntry = chunk[2];
chunk[2] = function (webpackRequire) {
originalEntry.apply(this, arguments);
injectEntries.forEach(webpackRequire);
};
} else {
chunk[2] = function (webpackRequire) {
injectEntries.forEach(webpackRequire);
};
}
break;
case "4":
if (chunk[2]?.[0]) {
chunk[2]?.[0].concat([injectEntries]);
} else {
chunk[2] = [injectEntries];
}
break;
}
}
console.log(chunk);
}
}
}
12 changes: 0 additions & 12 deletions src/config.js

This file was deleted.

26 changes: 22 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import config from "./config";
import { interceptWebpack } from "./patcher";

import Patcher from "./Patcher";
import { injectEverywhere } from "./injectEverywhere";


export const globalConfig = window.__webpackTools_config;
delete window.__webpackTools_config;

export const siteConfigs = new Set();
for (let siteConfig of globalConfig.siteConfigs) {
if (siteConfig.matchSites?.includes(window.location.host)) {
siteConfigs.add(siteConfig);
break;
}
}

// todo: magicrequire everywhere impl
if (config) {
interceptWebpack();
if (siteConfigs.size > 0) {
for (const siteConfig of siteConfigs) {
const patcher = new Patcher(siteConfig);
patcher.run();
}
} else if (globalConfig.wpToolsEverywhere) {
window.addEventListener("load", injectEverywhere);
}
54 changes: 54 additions & 0 deletions src/injectEverywhere.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { getWpToolsFunc } from "./wpTools";

function getVersion(chunkObject) {
if (chunkObject instanceof Array) {
return "modern";
} else {
return "legacy";
}
}

function injectWpTools(chunkObjectName) {
const chunkObject = window[chunkObjectName];

if (chunkObject.__wpt_everywhere_injected) {
return;
}
const version = getVersion(chunkObject);

console.log("[wpTools] Detected " + chunkObjectName + " using webpack " + version);

switch (version) {
case "modern":
// Gross Hack to support both webpack 4 and webpack 5
var load = function (webpackRequire) {
webpackRequire("wpTools");
};
load[0] = ["wpTools"];
load[Symbol.iterator] = function () {
return {
read: false,
next() {
if (!this.read) {
this.read = true;
return { done: false, value: 0 };
} else {
return { done: true };
}
},
};
};
chunkObject.__wpt_everywhere_injected = true
chunkObject.push([["wpTools"], { wpTools: getWpToolsFunc(chunkObjectName) }, load]);
break;
}
}

export function injectEverywhere() {
for (const key of Object.getOwnPropertyNames(window)) {
if ((key.includes("webpackJsonp") || key.includes("webpackChunk") || key.includes("__LOADABLE_LOADED_CHUNKS__")) && !key.startsWith("wpTools")) {
injectWpTools(key);
}
}
}

11 changes: 11 additions & 0 deletions src/matchModule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function matchModule(moduleStr, queryArg) {
const queryArray = queryArg instanceof Array ? queryArg : [queryArg];
return queryArray.some((query) => {
// we like our microoptimizations https://jsben.ch/Zk8aw
if (query instanceof RegExp) {
return query.test(moduleStr);
} else {
return moduleStr.includes(query);
}
});
}
Loading

0 comments on commit 1d1c8c3

Please sign in to comment.