Skip to content

Commit e3e3557

Browse files
authored
Genericize injectWebpackChunkLoadingMiddleware utility (#469)
It could come in handy for a variety of different extensions.
1 parent 92d04ee commit e3e3557

13 files changed

+234
-141
lines changed

workspaces/leetcode-zen-mode/src/extension/injectWebpackChunkLoadingMiddleware.ts

Lines changed: 0 additions & 98 deletions
This file was deleted.

workspaces/leetcode-zen-mode/src/extension/main.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { injectJsonParseMiddleware } from "@code-chronicles/util/browser-extensions/injectJsonParseMiddleware";
2+
import { injectWebpackChunkLoadingMiddleware } from "@code-chronicles/util/browser-extensions/injectWebpackChunkLoadingMiddleware";
23

3-
import { injectWebpackChunkLoadingMiddleware } from "./injectWebpackChunkLoadingMiddleware.ts";
4-
import { patchPageModule } from "./patchPageModule.ts";
4+
import { patchLeetCodeModule } from "./patchLeetCodeModule.ts";
55
import { rewriteLeetCodeGraphQLData } from "./rewriteLeetCodeGraphQLData.ts";
66

77
function main(): void {
@@ -15,8 +15,8 @@ function main(): void {
1515

1616
// Additionally, we will patch some of the actual page code! We will do so
1717
// by trying to intercept `webpack` chunk loading, so that we can patch the
18-
// packages used by the page.
19-
injectWebpackChunkLoadingMiddleware(patchPageModule);
18+
// modules used by the page.
19+
injectWebpackChunkLoadingMiddleware(patchLeetCodeModule);
2020
}
2121

2222
main();

workspaces/leetcode-zen-mode/src/extension/patchJsxFactory.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { isNonArrayObject } from "@code-chronicles/util/isNonArrayObject";
22
import { isString } from "@code-chronicles/util/isString";
3-
4-
function Null() {
5-
return null;
6-
}
3+
import { assignFunctionCosmeticProperties } from "@code-chronicles/util/object-properties/assignFunctionCosmeticProperties";
4+
import { NullReactElement } from "@code-chronicles/util/browser-extensions/NullReactElement";
75

86
type CreateElementFn = (
97
this: unknown,
@@ -15,8 +13,7 @@ type CreateElementFn = (
1513
export function patchJsxFactory(
1614
createElementFn: CreateElementFn,
1715
): CreateElementFn {
18-
// TODO: match the length of createElementFn
19-
return function (_elementType, props) {
16+
return assignFunctionCosmeticProperties(function (_elementType, props) {
2017
try {
2118
// Remove the Difficulty dropdown on `/problemset/`. The dropdown is
2219
// implemented as a React element with an `items` prop which is an
@@ -29,7 +26,7 @@ export function patchJsxFactory(
2926
isString(it.value) && /^easy$/i.test(it.value),
3027
)
3128
) {
32-
return createElementFn.apply(this, [Null, {}]);
29+
return createElementFn.apply(this, [NullReactElement, {}]);
3330
}
3431

3532
// Remove the non-Easy sections of the problems solved panel on user
@@ -40,7 +37,7 @@ export function patchJsxFactory(
4037
isString(props.category) &&
4138
/^(?:medium|hard)$/i.test(props.category)
4239
) {
43-
return createElementFn.apply(this, [Null, {}]);
40+
return createElementFn.apply(this, [NullReactElement, {}]);
4441
}
4542
} catch (err) {
4643
console.error(err);
@@ -51,5 +48,5 @@ export function patchJsxFactory(
5148
// Slight lie but `.apply` will work with the `arguments` object.
5249
arguments as unknown as Parameters<CreateElementFn>,
5350
);
54-
};
51+
}, createElementFn);
5552
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { isModuleTheReactPackage } from "@code-chronicles/util/browser-extensions/isModuleTheReactPackage";
2+
import { isModuleWebCellDomRenderer } from "@code-chronicles/util/browser-extensions/isModuleWebCellDomRenderer";
3+
4+
import { patchJsxFactory } from "./patchJsxFactory.ts";
5+
6+
const jsxs = new WeakSet();
7+
8+
export function patchLeetCodeModule(moduleExports: unknown): unknown {
9+
// LeetCode uses React for its website.
10+
if (isModuleTheReactPackage(moduleExports) && !jsxs.has(moduleExports)) {
11+
jsxs.add(moduleExports);
12+
moduleExports.createElement = patchJsxFactory(
13+
// TODO: remove the any
14+
moduleExports.createElement as any,
15+
);
16+
}
17+
18+
// LeetCode also uses a package which exposes `jsx` and `jsxs` methods,
19+
// seemingly https://github.com/EasyWebApp/DOM-Renderer
20+
if (isModuleWebCellDomRenderer(moduleExports) && !jsxs.has(moduleExports)) {
21+
jsxs.add(moduleExports);
22+
moduleExports.jsx =
23+
moduleExports.jsxs =
24+
moduleExports.jsxDEV =
25+
patchJsxFactory(
26+
// TODO: remove the any
27+
moduleExports.jsx as any,
28+
);
29+
}
30+
31+
return moduleExports;
32+
}

workspaces/leetcode-zen-mode/src/extension/patchPageModule.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

workspaces/leetcode-zen-mode/src/extension/rewriteLeetCodeGraphQLData.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { mapObjectValues } from "@code-chronicles/util/mapObjectValues";
44
import { stringToCase } from "@code-chronicles/util/stringToCase";
55

66
import { rewriteLeetCodeAggregateDataForDifficulty } from "./rewriteLeetCodeAggregateDataForDifficulty.ts";
7-
87
import { PREFERRED_STRING_CASE, STRING_CASE_CHECKERS } from "./stringCase.ts";
98

109
export function rewriteLeetCodeGraphQLData(value: unknown): unknown {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function NullReactElement() {
2+
return null;
3+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { assignFunctionCosmeticProperties } from "@code-chronicles/util/object-properties/assignFunctionCosmeticProperties";
2+
import { coalesceResults } from "@code-chronicles/util/coalesceResults";
3+
import { isString } from "@code-chronicles/util/isString";
4+
import { isNonArrayObject } from "@code-chronicles/util/isNonArrayObject";
5+
6+
type Middleware = (moduleFn: unknown) => unknown;
7+
8+
type Push = typeof Array.prototype.push;
9+
10+
/**
11+
* Injects middleware that runs when the current page is loading code chunks
12+
* built using `webpack`, allowing the patching of modules that make up the
13+
* page.
14+
*
15+
* Works when `webpack` was run using the "array-push" `output.chunkFormat`:
16+
* https://webpack.js.org/configuration/output/#outputchunkformat
17+
*/
18+
export function injectWebpackChunkLoadingMiddleware(
19+
middlewareFn: Middleware,
20+
): void {
21+
// `webpack`'s "array-push" chunk format works by pushing information about
22+
// chunks onto a globally defined array named something like
23+
// `webpackChunk_N_E`. The array is accessed as `self.webpackChunk_N_E`, so
24+
// we will temporarily proxy `globalThis.self` so we can detect attempts to
25+
// define this array, and inject some middleware into pushes into the array.
26+
const prevSelf = globalThis.self;
27+
28+
globalThis.self = new Proxy(prevSelf, {
29+
set(target, prop, newValue) {
30+
const res = Reflect.set(target, prop, newValue);
31+
32+
if (!(isString(prop) && prop.startsWith("webpackChunk"))) {
33+
return res;
34+
}
35+
36+
if (typeof newValue?.push !== "function") {
37+
// TODO: console.error something interesting
38+
return res;
39+
}
40+
41+
// Once we've found the magic `webpack` array we remove the proxy,
42+
// parts of the page seem to break without this.
43+
globalThis.self = prevSelf;
44+
45+
// The `webpack` bootstrapping code reassigns the array's `push`
46+
// method. We will intercept this reassignment so we can patch modules
47+
// before they are registered.
48+
let push: Push = newValue.push;
49+
Object.defineProperty(newValue, "push", {
50+
get() {
51+
return push;
52+
},
53+
54+
set<TNewPush extends Push>(newPush: TNewPush) {
55+
// This should never happen, but we'll defend.
56+
if (typeof newPush !== "function") {
57+
// TODO: console.error something interesting
58+
push = newPush;
59+
return;
60+
}
61+
62+
const wrappedNewPush = function (this: ThisParameterType<TNewPush>) {
63+
// In practice, `push` gets invoked with one chunk at a time,
64+
// but it's easy to not assume that, so we iterate over the
65+
// arguments.
66+
for (const arg of arguments) {
67+
// TODO: Array.isArray type refinement is unsafe, create a safer utility
68+
69+
// A chunk is structured as a tuple, the second element in the
70+
// tuple is an object map of numbers to functions implementing
71+
// each module. We will wrap these functions with our own code.
72+
const modules = arg?.[1];
73+
74+
for (const [key, moduleFn] of coalesceResults(
75+
() => Object.entries(modules),
76+
() => [],
77+
)) {
78+
if (typeof moduleFn !== "function") {
79+
// TODO: console.error something interesting
80+
continue;
81+
}
82+
83+
// `webpack` invokes module functions with a module object as
84+
// the first argument, and then the "exports" property of this
85+
// object is what was exported by the module (e.g. assigned to
86+
// `module.exports`).
87+
modules[key] = assignFunctionCosmeticProperties(function (
88+
this: unknown,
89+
) {
90+
const res = moduleFn.apply(this, arguments);
91+
92+
const module = arguments[0];
93+
if (
94+
isNonArrayObject(module) &&
95+
Object.hasOwn(module, "exports")
96+
) {
97+
// eslint-disable-next-line import-x/no-commonjs -- It's not our code that's CommonJS.
98+
module.exports = middlewareFn(module.exports);
99+
} else {
100+
console.error(
101+
"Surprising `webpack` module format: ",
102+
module,
103+
);
104+
}
105+
106+
return res;
107+
}, moduleFn);
108+
}
109+
}
110+
111+
return newPush.apply(
112+
this,
113+
// Slight lie but `.apply` will work with the `arguments` object.
114+
arguments as unknown as Parameters<TNewPush>,
115+
);
116+
};
117+
118+
push = assignFunctionCosmeticProperties(wrappedNewPush, newPush);
119+
},
120+
});
121+
122+
return res;
123+
},
124+
});
125+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { isNonArrayObject } from "@code-chronicles/util/isNonArrayObject";
2+
3+
// The core React package is some module with `createElement` and `useLayoutEffect` properties.
4+
type React = {
5+
createElement: Function;
6+
useLayoutEffect: Function;
7+
};
8+
9+
export function isModuleTheReactPackage(module: unknown): module is React {
10+
return (
11+
isNonArrayObject(module) &&
12+
typeof module.createElement === "function" &&
13+
typeof module.useLayoutEffect === "function"
14+
);
15+
}

0 commit comments

Comments
 (0)