Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

8 changes: 4 additions & 4 deletions workspaces/leetcode-zen-mode/src/extension/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { injectJsonParseMiddleware } from "@code-chronicles/util/browser-extensions/injectJsonParseMiddleware";
import { injectWebpackChunkLoadingMiddleware } from "@code-chronicles/util/browser-extensions/injectWebpackChunkLoadingMiddleware";

import { injectWebpackChunkLoadingMiddleware } from "./injectWebpackChunkLoadingMiddleware.ts";
import { patchPageModule } from "./patchPageModule.ts";
import { patchLeetCodeModule } from "./patchLeetCodeModule.ts";
import { rewriteLeetCodeGraphQLData } from "./rewriteLeetCodeGraphQLData.ts";

function main(): void {
Expand All @@ -15,8 +15,8 @@ function main(): void {

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

main();
15 changes: 6 additions & 9 deletions workspaces/leetcode-zen-mode/src/extension/patchJsxFactory.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { isNonArrayObject } from "@code-chronicles/util/isNonArrayObject";
import { isString } from "@code-chronicles/util/isString";

function Null() {
return null;
}
import { assignFunctionCosmeticProperties } from "@code-chronicles/util/object-properties/assignFunctionCosmeticProperties";
import { NullReactElement } from "@code-chronicles/util/browser-extensions/NullReactElement";

type CreateElementFn = (
this: unknown,
Expand All @@ -15,8 +13,7 @@ type CreateElementFn = (
export function patchJsxFactory(
createElementFn: CreateElementFn,
): CreateElementFn {
// TODO: match the length of createElementFn
return function (_elementType, props) {
return assignFunctionCosmeticProperties(function (_elementType, props) {
try {
// Remove the Difficulty dropdown on `/problemset/`. The dropdown is
// implemented as a React element with an `items` prop which is an
Expand All @@ -29,7 +26,7 @@ export function patchJsxFactory(
isString(it.value) && /^easy$/i.test(it.value),
)
) {
return createElementFn.apply(this, [Null, {}]);
return createElementFn.apply(this, [NullReactElement, {}]);
}

// Remove the non-Easy sections of the problems solved panel on user
Expand All @@ -40,7 +37,7 @@ export function patchJsxFactory(
isString(props.category) &&
/^(?:medium|hard)$/i.test(props.category)
) {
return createElementFn.apply(this, [Null, {}]);
return createElementFn.apply(this, [NullReactElement, {}]);
}
} catch (err) {
console.error(err);
Expand All @@ -51,5 +48,5 @@ export function patchJsxFactory(
// Slight lie but `.apply` will work with the `arguments` object.
arguments as unknown as Parameters<CreateElementFn>,
);
};
}, createElementFn);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { isModuleTheReactPackage } from "@code-chronicles/util/browser-extensions/isModuleTheReactPackage";
import { isModuleWebCellDomRenderer } from "@code-chronicles/util/browser-extensions/isModuleWebCellDomRenderer";

import { patchJsxFactory } from "./patchJsxFactory.ts";

const jsxs = new WeakSet();

export function patchLeetCodeModule(moduleExports: unknown): unknown {
// LeetCode uses React for its website.
if (isModuleTheReactPackage(moduleExports) && !jsxs.has(moduleExports)) {
jsxs.add(moduleExports);
moduleExports.createElement = patchJsxFactory(
// TODO: remove the any
moduleExports.createElement as any,
);
}

// LeetCode also uses a package which exposes `jsx` and `jsxs` methods,
// seemingly https://github.com/EasyWebApp/DOM-Renderer
if (isModuleWebCellDomRenderer(moduleExports) && !jsxs.has(moduleExports)) {
jsxs.add(moduleExports);
moduleExports.jsx =
moduleExports.jsxs =
moduleExports.jsxDEV =
patchJsxFactory(
// TODO: remove the any
moduleExports.jsx as any,
);
}

return moduleExports;
}
28 changes: 0 additions & 28 deletions workspaces/leetcode-zen-mode/src/extension/patchPageModule.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { mapObjectValues } from "@code-chronicles/util/mapObjectValues";
import { stringToCase } from "@code-chronicles/util/stringToCase";

import { rewriteLeetCodeAggregateDataForDifficulty } from "./rewriteLeetCodeAggregateDataForDifficulty.ts";

import { PREFERRED_STRING_CASE, STRING_CASE_CHECKERS } from "./stringCase.ts";

export function rewriteLeetCodeGraphQLData(value: unknown): unknown {
Expand Down
3 changes: 3 additions & 0 deletions workspaces/util/src/browser-extensions/NullReactElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function NullReactElement() {
return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { assignFunctionCosmeticProperties } from "@code-chronicles/util/object-properties/assignFunctionCosmeticProperties";
import { coalesceResults } from "@code-chronicles/util/coalesceResults";
import { isString } from "@code-chronicles/util/isString";
import { isNonArrayObject } from "@code-chronicles/util/isNonArrayObject";

type Middleware = (moduleFn: unknown) => unknown;

type Push = typeof Array.prototype.push;

/**
* Injects middleware that runs when the current page is loading code chunks
* built using `webpack`, allowing the patching of modules that make up the
* page.
*
* Works when `webpack` was run using the "array-push" `output.chunkFormat`:
* https://webpack.js.org/configuration/output/#outputchunkformat
*/
export function injectWebpackChunkLoadingMiddleware(
middlewareFn: Middleware,
): void {
// `webpack`'s "array-push" chunk format works by pushing information about
// chunks onto a globally defined array named something like
// `webpackChunk_N_E`. The array is accessed as `self.webpackChunk_N_E`, so
// we will temporarily proxy `globalThis.self` so we can detect attempts to
// define this array, and inject some middleware into pushes into the array.
const prevSelf = globalThis.self;

globalThis.self = new Proxy(prevSelf, {
set(target, prop, newValue) {
const res = Reflect.set(target, prop, newValue);

if (!(isString(prop) && prop.startsWith("webpackChunk"))) {
return res;
}

if (typeof newValue?.push !== "function") {
// TODO: console.error something interesting
return res;
}

// Once we've found the magic `webpack` array we remove the proxy,
// parts of the page seem to break without this.
globalThis.self = prevSelf;

// The `webpack` bootstrapping code reassigns the array's `push`
// method. We will intercept this reassignment so we can patch modules
// before they are registered.
let push: Push = newValue.push;
Object.defineProperty(newValue, "push", {
get() {
return push;
},

set<TNewPush extends Push>(newPush: TNewPush) {
// This should never happen, but we'll defend.
if (typeof newPush !== "function") {
// TODO: console.error something interesting
push = newPush;
return;
}

const wrappedNewPush = function (this: ThisParameterType<TNewPush>) {
// In practice, `push` gets invoked with one chunk at a time,
// but it's easy to not assume that, so we iterate over the
// arguments.
for (const arg of arguments) {
// TODO: Array.isArray type refinement is unsafe, create a safer utility

// A chunk is structured as a tuple, the second element in the
// tuple is an object map of numbers to functions implementing
// each module. We will wrap these functions with our own code.
const modules = arg?.[1];

for (const [key, moduleFn] of coalesceResults(
() => Object.entries(modules),
() => [],
)) {
if (typeof moduleFn !== "function") {
// TODO: console.error something interesting
continue;
}

// `webpack` invokes module functions with a module object as
// the first argument, and then the "exports" property of this
// object is what was exported by the module (e.g. assigned to
// `module.exports`).
modules[key] = assignFunctionCosmeticProperties(function (
this: unknown,
) {
const res = moduleFn.apply(this, arguments);

const module = arguments[0];
if (
isNonArrayObject(module) &&
Object.hasOwn(module, "exports")
) {
// eslint-disable-next-line import-x/no-commonjs -- It's not our code that's CommonJS.
module.exports = middlewareFn(module.exports);
} else {
console.error(
"Surprising `webpack` module format: ",
module,
);
}

return res;
}, moduleFn);
}
}

return newPush.apply(
this,
// Slight lie but `.apply` will work with the `arguments` object.
arguments as unknown as Parameters<TNewPush>,
);
};

push = assignFunctionCosmeticProperties(wrappedNewPush, newPush);
},
});

return res;
},
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { isNonArrayObject } from "@code-chronicles/util/isNonArrayObject";

// The core React package is some module with `createElement` and `useLayoutEffect` properties.
type React = {
createElement: Function;
useLayoutEffect: Function;
};

export function isModuleTheReactPackage(module: unknown): module is React {
return (
isNonArrayObject(module) &&
typeof module.createElement === "function" &&
typeof module.useLayoutEffect === "function"
);
}
Loading
Loading