Skip to content

Commit a18b896

Browse files
authored
Process __NEXT_DATA__ in Chrome extension (#467)
The LeetCode study plans (e.g. https://leetcode.com/studyplan/top-100-liked/) get their data in this way, so we inject middleware here for this use case as well.
1 parent a42843d commit a18b896

File tree

3 files changed

+70
-0
lines changed

3 files changed

+70
-0
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import invariant from "invariant";
2+
import type { JsonValue } from "type-fest";
3+
4+
import { jsonParseSafe } from "@code-chronicles/util/jsonParseSafe";
5+
6+
type ProxiedPropertyKey = "innerHTML" | "innerText" | "textContent";
7+
8+
type MiddlewareFn = (
9+
data: JsonValue,
10+
script: HTMLScriptElement,
11+
property: ProxiedPropertyKey,
12+
) => JsonValue;
13+
14+
function inject<TProto extends { constructor: Function }>(
15+
proto: TProto,
16+
property: ProxiedPropertyKey,
17+
middlewareFn: MiddlewareFn,
18+
): void {
19+
const prevDescriptor = Object.getOwnPropertyDescriptor(proto, property);
20+
21+
invariant(
22+
prevDescriptor && prevDescriptor.get,
23+
`\`${proto.constructor.name}.prototype.${property}\` property descriptor didn't have the expected form!`,
24+
);
25+
const prevDescriptorGet = prevDescriptor.get;
26+
27+
Object.defineProperty(HTMLScriptElement.prototype, property, {
28+
...prevDescriptor,
29+
get(this: HTMLScriptElement) {
30+
const data = prevDescriptorGet.call(this);
31+
32+
// If the data doesn't parse as JSON, we pass it through unchanged.
33+
// If it does parse as JSON, we'll run the middleware.
34+
const parsedData = jsonParseSafe(data);
35+
if (parsedData) {
36+
return JSON.stringify(middlewareFn(parsedData.data, this, property));
37+
}
38+
39+
return data;
40+
},
41+
});
42+
}
43+
44+
/**
45+
* Injects a function to process and possibly replace JSON data stored within
46+
* <script> tags. The middleware will run when properties such as `innerHTML`
47+
* get accessed, and it must return the possibly updated JSON data.
48+
*/
49+
export function injectJsonScriptMiddleware(middlewareFn: MiddlewareFn): void {
50+
inject(Element.prototype, "innerHTML", middlewareFn);
51+
inject(HTMLElement.prototype, "innerText", middlewareFn);
52+
inject(Node.prototype, "textContent", middlewareFn);
53+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { mapJsonBlobData } from "@code-chronicles/util/mapJsonBlobData";
22

3+
import { injectJsonScriptMiddleware } from "./injectJsonScriptMiddleware.ts";
34
import { injectXhrBlobResponseMiddleware } from "./injectXhrBlobResponseMiddleware.ts";
45
import { patchWebpackChunkLoading } from "./patchWebpackChunkLoading.ts";
56
import { rewriteLeetCodeGraphQLData } from "./rewriteLeetCodeGraphQLData.ts";
@@ -23,6 +24,11 @@ function main() {
2324
return blob;
2425
});
2526

27+
// LeetCode also reads data stored as JSON within some <script> tags within
28+
// the page, for example the __NEXT_DATA__ used by Next.js. We will inject
29+
// the middleware there as well.
30+
injectJsonScriptMiddleware(rewriteLeetCodeGraphQLData);
31+
2632
// Additionally, we will patch some of the actual page code! We will do so
2733
// by trying to intercept `webpack` chunk loading, so that we can patch the
2834
// modules used by the page.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { JsonValue } from "type-fest";
2+
3+
export function jsonParseSafe(
4+
...args: Parameters<typeof JSON.parse>
5+
): { data: JsonValue } | undefined {
6+
try {
7+
return { data: JSON.parse(...args) };
8+
} catch {
9+
return undefined;
10+
}
11+
}

0 commit comments

Comments
 (0)