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
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ function generateZod(
],
];
}
case "ID": {
case "ID":
case "JSONString": {
invariant(directives.length === 0, "Directives not supported here.");
return [new ZodOutput("z.string()", true), []];
}
Expand Down
1 change: 0 additions & 1 deletion workspaces/leetcode-zen-mode/src/extension/constants.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import invariant from "invariant";
import type { Promisable } from "type-fest";

import { mapArrayAtIndex } from "@code-chronicles/util/mapArrayAtIndex";

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

// TODO: maybe support a list of middleware functions?
// TODO: support other ways to read the data besides

/**
* Injects a function to process and possibly replace `XMLHttpRequest`
* responses (if they are of type `Blob`) when there is an attempt to read
* their data.
*
* The middleware function may be synchronous or asynchronous. If it is
* asynchronous, the read attempt is blocked until the middleware function
* resolves. The middleware can replace the data by returning or resolving to
* a different `Blob` from the one that was passed in. To leave the data
* as-is, resolve either `undefined` or the the original `Blob`.
*
* Note: Currently only attempts to read the data through a `FileReader`'s
* `readAsText` method are detected, but more types of reads may be supported
* in the future.
*/
export function injectXhrBlobResponseMiddleware(
middlewareFn: (
xhr: XMLHttpRequest,
blob: Blob,
encoding?: string,
) => Promisable<Blob | void | undefined>,
): void {
const getXhrForBlob = startRecordingXhrResponseBlobs();

const prevDescriptor = Object.getOwnPropertyDescriptor(
FileReader.prototype,
"readAsText",
);

invariant(
prevDescriptor && prevDescriptor.value && prevDescriptor.writable,
"`FileReader.prototype.readAsText` property descriptor didn't have the expected form!",
);

const prevImplementation: typeof FileReader.prototype.readAsText =
prevDescriptor.value;

FileReader.prototype.readAsText = function readAsText(
this: FileReader,
originalBlob: Blob,
encoding?: string,
): void {
const args = [...arguments] as Parameters<
typeof FileReader.prototype.readAsText
>;
const xhr = getXhrForBlob(originalBlob);

if (!xhr) {
prevImplementation.apply(this, args);
return;
}

Promise.resolve(middlewareFn(xhr, originalBlob, encoding)).then(
(possiblyUpdatedBlob) => {
prevImplementation.apply(
this,
mapArrayAtIndex(args, 0, () => possiblyUpdatedBlob ?? originalBlob),
);
},
);
};
}
25 changes: 18 additions & 7 deletions workspaces/leetcode-zen-mode/src/extension/main.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import { patchFileReader } from "./patchFileReader.ts";
import { mapJsonBlobData } from "@code-chronicles/util/mapJsonBlobData";

import { injectXhrBlobResponseMiddleware } from "./injectXhrBlobResponseMiddleware.ts";
import { patchWebpackChunkLoading } from "./patchWebpackChunkLoading.ts";
import { patchXhr } from "./patchXhr.ts";
import { rewriteGraphQLData } from "./rewriteGraphQLData.ts";

function main() {
// LeetCode's GraphQL client makes requests through `XMLHttpRequest`, then
// reads the data as `Blob` objects using the `FileReader` API.
//
// So we will label `Blob` objects from `XMLHttpRequest` responses with a
// special symbol, and then later, when reading from the marked `Blob`
// objects, we will rewrite the data a bit.
patchXhr();
patchFileReader();
// So we will inject some middleware to rewrite `XMLHttpRequest` responses
// a bit.
injectXhrBlobResponseMiddleware((xhr, blob) => {
if (xhr.responseURL === "https://leetcode.com/graphql/") {
try {
return mapJsonBlobData(blob, rewriteGraphQLData);
} catch (err) {
console.error(err);
}
}

// No-op for requests that aren't for LeetCode's GraphQL endpoint.
return blob;
});

// 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
Expand Down
29 changes: 0 additions & 29 deletions workspaces/leetcode-zen-mode/src/extension/patchFileReader.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function patchPageModule<TThis, TArgs extends unknown[], TRes>(

// LeetCode also uses a module which exposes `jsx` and `jsxs` methods,
// possibly https://web-cell.dev/
if (Object.hasOwn(module, "jsx")) {
if (Object.hasOwn(module, "jsx") && !jsxs.has(module)) {
jsxs.add(module);
module.jsx = module.jsxs = patchJsxFactory(module.jsx);
}
Expand Down
36 changes: 0 additions & 36 deletions workspaces/leetcode-zen-mode/src/extension/patchXhr.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { JsonValue } from "type-fest";

import { isObject } from "@code-chronicles/util/isObject";
import { mapObjectValues } from "@code-chronicles/util/mapObjectValues";

export function rewriteGraphQLData(value: unknown): unknown {
export function rewriteGraphQLData(value: JsonValue): JsonValue {
if (Array.isArray(value)) {
return value.map(rewriteGraphQLData);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import invariant from "invariant";

type GetXhrForBlob = (blob: Blob) => XMLHttpRequest | undefined;

let getXhrForBlob: GetXhrForBlob | undefined = undefined;

/**
* Patches the `XMLHttpRequest` class so that we can record when the response
* of an `XMLHttpRequest` instance is a `Blob`. Returns a function that can be
* used to look up the `XMLHttpRequest` from which a particular `Blob`
* originated.
*/
export function startRecordingXhrResponseBlobs(): GetXhrForBlob {
// TODO: use a memoize function!
return (getXhrForBlob ??= (() => {
const prevDescriptor = Object.getOwnPropertyDescriptor(
XMLHttpRequest.prototype,
"response",
);

invariant(
prevDescriptor && prevDescriptor.get && !prevDescriptor.set,
"`XMLHttpRequest.prototype.response` property descriptor didn't have the expected form!",
);
const prevDescriptorGet: () => typeof XMLHttpRequest.prototype.response =
prevDescriptor.get;

const xhrResponses = new WeakMap<Blob, XMLHttpRequest>();

Object.defineProperty(XMLHttpRequest.prototype, "response", {
...prevDescriptor,
get(this: XMLHttpRequest) {
const res = prevDescriptorGet.call(this);

if (res instanceof Blob) {
xhrResponses.set(res, this);
}

return res;
},
});

return (blob) => xhrResponses.get(blob);
})());
}
7 changes: 7 additions & 0 deletions workspaces/util/src/constructJsonBlob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { JsonValue } from "type-fest";

export const JSON_MIME_TYPE = "application/json";

export function constructJsonBlob(data: JsonValue): Blob {
return new Blob([JSON.stringify(data)], { type: JSON_MIME_TYPE });
}
19 changes: 13 additions & 6 deletions workspaces/util/src/mapArrayAtIndex.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
export function mapArrayAtIndex<T>(
arr: readonly T[],
index: number,
mapFn: (oldValue: T) => T,
): T[] {
import type { Writable } from "type-fest";

export function mapArrayAtIndex<
TArray extends readonly unknown[],
TIndex extends number,
>(
arr: TArray,
index: TIndex,
mapFn: (oldValue: TArray[TIndex]) => TArray[TIndex],
): Writable<TArray> {
if (index < 0) {
throw new RangeError(
"Mapping out of bounds index " +
Expand All @@ -13,5 +18,7 @@ export function mapArrayAtIndex<T>(
);
}

return arr.map((value, i) => (i === index ? mapFn(value) : value));
return arr.map((value, i) =>
i === index ? mapFn(value) : value,
) as unknown as Writable<TArray>;
}
11 changes: 11 additions & 0 deletions workspaces/util/src/mapJsonBlobData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { JsonValue } from "type-fest";

import { constructJsonBlob } from "@code-chronicles/util/constructJsonBlob";

export async function mapJsonBlobData(
blob: Blob,
mapFn: (data: JsonValue) => JsonValue,
): Promise<Blob> {
const data: JsonValue = JSON.parse(await blob.text());
return constructJsonBlob(mapFn(data));
}
Loading