From 6162dfd08b2d5a70f2c32664fdb42c42e781f06b Mon Sep 17 00:00:00 2001
From: ryan-0324 <77452312+ryan-0324@users.noreply.github.com>
Date: Sat, 4 May 2024 23:33:53 -0400
Subject: [PATCH] refactor+chore: metro & misc. (#29)
* Refactored `lib/metro` and added types + cleaned up `tsconfig.json` + minor changes to `build.mjs` and `package.json`
* Made requested changes + added eslint rule to restrict top-level await
* Exclude `transform-async-to-generator` from `env` (async/await is es7)
---
.eslintrc.json | 6 +-
build.mjs | 25 ++--
package.json | 18 +--
src/lib/metro/filters.ts | 178 +++++++++++++++-------------
src/lib/metro/index.ts | 5 +-
src/lib/metro/polyfills/redesign.ts | 18 ++-
src/lib/metro/types.ts | 94 ++++++++++++++-
src/lib/metro/utils.ts | 11 ++
src/lib/utils/types.ts | 2 +
tsconfig.json | 48 ++++----
10 files changed, 264 insertions(+), 141 deletions(-)
create mode 100644 src/lib/metro/utils.ts
diff --git a/.eslintrc.json b/.eslintrc.json
index f2dd2e1..ac03347 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -9,6 +9,10 @@
"import-alias"
],
"rules": {
+ "no-restricted-syntax": [ "error", {
+ "selector": "AwaitExpression:not(:function *)",
+ "message": "Hermes does not support top-level await, and SWC cannot transform it."
+ } ],
"quotes": [ "error", "double", { "avoidEscape": true } ],
"jsx-quotes": [ "error", "prefer-double" ],
"no-mixed-spaces-and-tabs": "error",
@@ -72,4 +76,4 @@
}
]
}
-}
\ No newline at end of file
+}
diff --git a/build.mjs b/build.mjs
index 12bff34..b05af40 100644
--- a/build.mjs
+++ b/build.mjs
@@ -6,6 +6,7 @@ import { readFile } from "fs/promises";
import { createServer } from "http";
import { argv } from "process";
+// @ts-ignore
const isFlag = (s, l) => argv.slice(2).some(c => c === `-${s}` || c === `--${l}`);
const isRelease = isFlag("r", "release");
@@ -16,7 +17,7 @@ const commitHash = execSync("git rev-parse --short HEAD").toString().trim();
try {
const ctx = await esbuild.context({
- entryPoints: ["src/entry.js"],
+ entryPoints: ["./src/entry.js"],
bundle: true,
minify: isRelease,
format: "iife",
@@ -33,13 +34,6 @@ try {
},
loader: { ".png": "dataurl" },
legalComments: "none",
- alias: {
- "@lib/*": "src/lib/*",
- "@core/*": "src/core/*",
- "@metro/*": "src/lib/metro/*",
- "@ui/*": "src/lib/ui/*",
- "@types": "src/utils/types.ts",
- },
plugins: [
{
name: "runtimeGlobalAlias",
@@ -55,6 +49,7 @@ try {
namespace: "glob-" + key, path: args.path
}));
build.onLoad({ filter, namespace: "glob-" + key }, () => ({
+ // @ts-ignore
contents: `Object.defineProperty(module, 'exports', { get: () => ${globalMap[key]} })`,
resolveDir: "src",
}));
@@ -99,16 +94,22 @@ try {
}
},
},
+ // https://github.com/facebook/hermes/blob/3815fec63d1a6667ca3195160d6e12fee6a0d8d5/doc/Features.md
+ // https://github.com/facebook/hermes/issues/696#issuecomment-1396235791
env: {
- targets: "defaults",
+ targets: "fully supports es6",
include: [
- "transform-classes",
"transform-arrow-functions",
"transform-block-scoping",
- "transform-class-properties"
+ "transform-classes"
],
exclude: [
- "transform-parameters"
+ "transform-async-to-generator",
+ "transform-exponentiation-operator",
+ "transform-named-capturing-groups-regex",
+ "transform-nullish-coalescing-operator",
+ "transform-object-rest-spread",
+ "transform-optional-chaining"
]
},
});
diff --git a/package.json b/package.json
index f009de4..b2d7207 100644
--- a/package.json
+++ b/package.json
@@ -21,16 +21,17 @@
"license": "BSD-3-Clause",
"devDependencies": {
"@react-native-clipboard/clipboard": "1.10.0",
- "@swc/core": "^1.4.6",
- "@types/chroma-js": "^2.4.4",
- "@types/lodash": "^4.14.202",
+ "@swc/core": "^1.4.17",
+ "@swc/helpers": "^0.5.11",
+ "@types/chroma-js": "~2.4.0",
+ "@types/lodash": "~4.17.0",
"@types/node": "^20.11.25",
- "@types/react": "18.2.60",
- "@types/react-native": "0.72.3",
+ "@types/react": "~18.2.0",
+ "@types/react-native": "~0.72.0",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"@typescript-eslint/typescript-estree": "^7.1.1",
- "esbuild": "^0.17.19",
+ "esbuild": "^0.20.2",
"eslint": "^8.57.0",
"eslint-plugin-import-alias": "^1.2.0",
"eslint-plugin-react": "^7.34.0",
@@ -39,11 +40,10 @@
"intl-messageformat": "^10.5.11",
"moment": "2.22.2",
"type-fest": "^4.12.0",
- "typescript": "^5.4.2",
+ "typescript": "^5.4.5",
"typescript-transform-paths": "^3.4.7"
},
"dependencies": {
- "@swc/helpers": "0.5.0",
"fuzzysort": "^2.0.4",
"spitroast": "^1.4.4"
},
@@ -55,4 +55,4 @@
]
}
}
-}
\ No newline at end of file
+}
diff --git a/src/lib/metro/filters.ts b/src/lib/metro/filters.ts
index e6af7bb..b45b359 100644
--- a/src/lib/metro/filters.ts
+++ b/src/lib/metro/filters.ts
@@ -1,81 +1,67 @@
import { instead } from "@lib/api/patcher";
+import type { Metro } from "@metro/types";
+import { metroRequire, modules } from "@metro/utils";
-export type MetroModules = { [id: string]: any; };
-export type PropIntellisense
= Record
& Record;
-export type PropsFinder = (...props: T[]) => PropIntellisense;
-export type PropsFinderAll = (...props: T[]) => PropIntellisense[];
+export type PropIntellisense = Record
& Record;
+export type PropsFinder = (...props: T[]) => PropIntellisense;
+export type PropsFinderAll = (...props: T[]) => PropIntellisense[];
-interface MetroRequire {
- (moduleId: string): any;
- importDefault(moduleId: string): any;
- importAll(moduleId: string): any;
+/** Makes the module associated with the specified ID non-enumberable. */
+function blacklistModule(id: Metro.ModuleID) {
+ Object.defineProperty(modules, id, { enumerable: false });
}
-// Metro global vars
-declare var __r: MetroRequire;
-declare var modules: MetroModules;
-
-// Function to blacklist a module, preventing it from being searched again
-const blacklist = (id: string) => Object.defineProperty(window.modules, id, {
- value: window.modules[id],
- enumerable: false,
- configurable: true,
- writable: true
-});
-
const functionToString = Function.prototype.toString;
-for (const id in window.modules) {
- const moduleDefinition = window.modules[id];
+for (const id in modules) {
+ const metroModule = modules[id];
- if (moduleDefinition.factory) {
- instead("factory", moduleDefinition, (args, orig) => {
+ if (metroModule!.factory) {
+ instead("factory", metroModule, ((args: Parameters, origFunc: Metro.FactoryFn) => {
const { 1: metroRequire, 4: moduleObject } = args;
- args[2 /* metroImportDefault */] = (id: string) => {
- const exp = metroRequire(id);
- return exp && exp.__esModule ? exp.default : exp;
+ args[2 /* metroImportDefault */] = id => {
+ const exps = metroRequire(id);
+ return exps && exps.__esModule ? exps.default : exps;
};
- args[3 /* metroImportAll */] = (id: string) => {
- const exp = metroRequire(id);
- if (exp && exp.__esModule) return exp;
+ args[3 /* metroImportAll */] = id => {
+ const exps = metroRequire(id);
+ if (exps && exps.__esModule) return exps;
const importAll: Record = {};
- if (exp) Object.assign(importAll, exp);
- return importAll.default = exp, importAll;
+ if (exps) Object.assign(importAll, exps);
+ importAll.default = exps;
+ return importAll;
};
- orig(...args);
+ origFunc(...args);
if (moduleObject.exports) onModuleRequire(moduleObject.exports);
- });
+ }) as any); // If only spitroast had better types
}
}
// Blacklist any "bad-actor" modules, e.g. the dreaded null proxy, the window itself, or undefined modules
-for (const id in window.modules) {
- const module = requireModule(id);
+for (const id in modules) {
+ const moduleExports = requireModule(id);
- if (!module || module === window || module["Revenge EOL when?"] === null) {
- blacklist(id);
- continue;
- }
+ if (!moduleExports || moduleExports === window || moduleExports["Revenge EOL when?"] === null)
+ blacklistModule(id);
}
let patchedInspectSource = false;
-function onModuleRequire(exports: any) {
+function onModuleRequire(moduleExports: any) {
// Temporary
- exports.initSentry &&= () => { };
- if (exports.default?.track && exports.default.trackMaker) {
- exports.default.track = () => Promise.resolve();
- }
+ moduleExports.initSentry &&= () => undefined;
+ if (moduleExports.default?.track && moduleExports.default.trackMaker)
+ moduleExports.default.track = () => Promise.resolve();
// There are modules registering the same native component
- if (exports?.default?.name === "requireNativeComponent") {
- instead("default", exports, (args, orig) => {
+ if (moduleExports?.default?.name === "requireNativeComponent") {
+ instead("default", moduleExports, (args, origFunc) => {
try {
- return orig(...args);
+ return origFunc(...args);
} catch {
return args[0];
}
@@ -83,9 +69,9 @@ function onModuleRequire(exports: any) {
}
// Hook DeveloperExperimentStore
- if (exports?.default?.constructor?.displayName === "DeveloperExperimentStore") {
- exports.default = new Proxy(exports.default, {
- get: (target, property, receiver) => {
+ if (moduleExports?.default?.constructor?.displayName === "DeveloperExperimentStore") {
+ moduleExports.default = new Proxy(moduleExports.default, {
+ get(target, property, receiver) {
if (property === "isDeveloper") {
// Hopefully won't explode accessing it here :3
const { settings } = require("@lib/settings");
@@ -99,66 +85,90 @@ function onModuleRequire(exports: any) {
// Funny infinity recursion caused by a race condition
if (!patchedInspectSource && window["__core-js_shared__"]) {
- const inspect = (f: Function) => typeof f === "function" && functionToString.apply(f, []);
+ const inspect = (f: unknown) => typeof f === "function" && functionToString.apply(f, []);
window["__core-js_shared__"].inspectSource = inspect;
patchedInspectSource = true;
}
}
-function requireModule(id: string) {
- if (modules[id].isInitialized) return __r(id);
+const noopHandler = () => undefined;
+
+function requireModule(id: Metro.ModuleID) {
+ if (modules[id]!.isInitialized) return metroRequire(id);
// Disable Internal Metro error reporting logic
- const originalHandler = window.ErrorUtils.getGlobalHandler();
- window.ErrorUtils.setGlobalHandler(null);
+ const originalHandler = ErrorUtils.getGlobalHandler();
+ ErrorUtils.setGlobalHandler(noopHandler);
+ let moduleExports;
try {
- var exports = __r(id); // metroRequire(id);
+ moduleExports = metroRequire(id);
} catch {
- var exports = undefined;
+ moduleExports = undefined;
}
// Done initializing! Now, revert our hacks
- window.ErrorUtils.setGlobalHandler(originalHandler);
+ ErrorUtils.setGlobalHandler(originalHandler);
- return exports;
+ return moduleExports;
}
-function* getModules() {
- yield require("./polyfills/redesign");
+function* getModuleExports() {
+ yield require("@metro/polyfills/redesign");
for (const id in modules) {
yield requireModule(id);
}
}
-// Function to filter through modules
-const filterModules = (modules: MetroModules, single = false) => (filter: (m: any) => boolean) => {
- const found = [];
+type ExportsFilter = (moduleExports: any) => unknown;
- for (const exports of getModules()) {
- if (exports.default && exports.__esModule && filter(exports.default)) {
- if (single) return exports.default;
- found.push(exports.default);
- }
+function testExports(moduleExports: any, filter: ExportsFilter) {
+ if (moduleExports.default && moduleExports.__esModule && filter(moduleExports.default))
+ return moduleExports.default;
+ if (filter(moduleExports))
+ return moduleExports;
+}
- if (filter(exports)) {
- if (single) return exports;
- else found.push(exports);
- }
+/**
+ * Returns the exports of the first module where filter returns truthy, and undefined otherwise.
+ * @param filter find calls filter once for each enumerable module's exports until it finds one where filter returns a thruthy value.
+ */
+export function find(filter: ExportsFilter) {
+ for (const moduleExports of getModuleExports()) {
+ const testedExports = testExports(moduleExports, filter);
+ if (testedExports !== undefined)
+ return testedExports;
}
+}
- if (!single) return found;
-};
+/**
+ * Returns the exports of all modules where filter returns truthy.
+ * @param filter findAll calls filter once for each enumerable module's exports, adding the exports to the returned array when filter returns a thruthy value.
+ */
+export function findAll(filter: ExportsFilter) {
+ const foundExports: any[] = [];
+ for (const moduleExports of getModuleExports()) {
+ const testedExports = testExports(moduleExports, filter);
+ if (testedExports !== undefined)
+ foundExports.push(testedExports);
+ }
+ return foundExports;
+}
-export const find = filterModules(modules, true);
-export const findAll = filterModules(modules);
-
-const propsFilter = (props: (string | symbol)[]) => (m: any) => props.every(p => m[p] !== undefined);
-const nameFilter = (name: string, defaultExp: boolean) => (defaultExp ? (m: any) => m?.name === name : (m: any) => m?.default?.name === name);
-const dNameFilter = (displayName: string, defaultExp: boolean) => (defaultExp ? (m: any) => m?.displayName === displayName : (m: any) => m?.default?.displayName === displayName);
-const tNameFilter = (typeName: string, defaultExp: boolean) => (defaultExp ? (m: any) => m?.type?.name === typeName : (m: any) => m?.default?.type?.name === typeName);
-const storeFilter = (name: string) => (m: any) => m.getName && m.getName.length === 0 && m.getName() === name;
+const propsFilter = (props: PropertyKey[]) =>
+ (exps: any) => props.every(p => exps[p] !== undefined);
+const nameFilter = (name: string, defaultExp: boolean) => defaultExp
+ ? (exps: any) => exps?.name === name
+ : (exps: any) => exps?.default?.name === name;
+const dNameFilter = (displayName: string, defaultExp: boolean) => defaultExp
+ ? (exps: any) => exps?.displayName === displayName
+ : (exps: any) => exps?.default?.displayName === displayName;
+const tNameFilter = (typeName: string, defaultExp: boolean) => defaultExp
+ ? (exps: any) => exps?.type?.name === typeName
+ : (exps: any) => exps?.default?.type?.name === typeName;
+const storeFilter = (name: string) =>
+ (exps: any) => exps.getName && exps.getName.length === 0 && exps.getName() === name;
export const findByProps: PropsFinder = (...props) => find(propsFilter(props));
export const findByPropsAll: PropsFinderAll = (...props) => findAll(propsFilter(props));
diff --git a/src/lib/metro/index.ts b/src/lib/metro/index.ts
index bc2ad68..70391bc 100644
--- a/src/lib/metro/index.ts
+++ b/src/lib/metro/index.ts
@@ -1,4 +1,4 @@
-export * as common from "./common";
+export * from "./utils";
export {
find,
findAll,
@@ -10,6 +10,7 @@ export {
findByPropsAll,
findByStoreName,
findByTypeName,
- findByTypeNameAll,
+ findByTypeNameAll
} from "./filters";
export * as filters from "./filters";
+export * as common from "./common";
diff --git a/src/lib/metro/polyfills/redesign.ts b/src/lib/metro/polyfills/redesign.ts
index 0d54786..fa07b6e 100644
--- a/src/lib/metro/polyfills/redesign.ts
+++ b/src/lib/metro/polyfills/redesign.ts
@@ -1,4 +1,4 @@
-import "@metro/index";
+import { metroRequire, modules } from "@metro";
import { LiteralUnion } from "type-fest";
@@ -103,25 +103,23 @@ type Keys = LiteralUnion ? U : string,
const redesignPropSource = {} as Record;
const redesignModule = {} as Record;
-for (const id in window.modules) {
- const exports = window.__r(id);
- polyfillRedesignModule(exports);
+for (const id in modules) {
+ const moduleExports = metroRequire(id);
+ polyfillRedesignModule(moduleExports);
}
-function polyfillRedesignModule(exports: any) {
+function polyfillRedesignModule(moduleExports: any) {
const propMap = new Map();
for (const prop of redesignProps) {
- if (exports?.[prop]) {
+ if (moduleExports?.[prop])
propMap.set(prop, null);
- }
- else if (exports?.default?.[prop]) {
+ else if (moduleExports?.default?.[prop])
propMap.set(prop, "default");
- }
}
for (const [prop, defaultKey] of propMap) {
- const exportsForProp = defaultKey ? exports[defaultKey] : exports;
+ const exportsForProp = defaultKey ? moduleExports[defaultKey] : moduleExports;
if (redesignModule[prop]) {
if (Object.keys(exportsForProp).length < Object.keys(redesignPropSource[prop]).length) {
diff --git a/src/lib/metro/types.ts b/src/lib/metro/types.ts
index adf1c65..3b2d6b6 100644
--- a/src/lib/metro/types.ts
+++ b/src/lib/metro/types.ts
@@ -1,3 +1,96 @@
+import type { Nullish } from "@types";
+
+/** @see {@link https://github.com/facebook/metro/blob/c2d7539dfc10aacb2f99fcc2f268a3b53e867a90/packages/metro-runtime/src/polyfills/require.js} */
+export namespace Metro {
+ export interface DependencyMap {
+ readonly [indexer: number]: ModuleID;
+ readonly paths?: Readonly> | undefined;
+ }
+
+ export type InverseDependencyMap = Record;
+
+ export type FactoryFn = (
+ global: object,
+ require: RequireFn,
+ metroImportDefault: RequireFn,
+ metroImportAll: RequireFn,
+ moduleObject: {
+ exports: any;
+ },
+ exports: any,
+ dependencyMap: DependencyMap | Nullish,
+ ) => void;
+
+ export interface HotModuleReloadingData {
+ _acceptCallback: (() => void) | Nullish;
+ _disposeCallback: (() => void) | Nullish;
+ _didAccept: boolean;
+ accept: (callback?: (() => void) | undefined) => void;
+ dispose: (callback?: (() => void) | undefined) => void;
+ }
+
+ export type ModuleID = string | number;
+
+ export interface Module {
+ id?: ModuleID | undefined;
+ exports: any;
+ hot?: HotModuleReloadingData | undefined;
+ }
+
+ export interface ModuleDefinition {
+ dependencyMap: DependencyMap | Nullish;
+ error?: any;
+ factory: FactoryFn | undefined;
+ hasError: boolean;
+ hot?: HotModuleReloadingData | undefined;
+ importedAll: any;
+ importedDefault: any;
+ isInitialized: boolean;
+ path?: string | undefined;
+ publicModule: Module;
+ verboseName?: string | undefined;
+ }
+
+ export type ModuleList = Record;
+
+ export type RequireFn = (id: ModuleID) => any;
+
+ export type DefineFn = (
+ factory: FactoryFn,
+ moduleId: ModuleID,
+ dependencyMap?: DependencyMap | undefined,
+ verboseName?: string | undefined,
+ inverseDependencies?: InverseDependencyMap | undefined
+ ) => void;
+
+ export type ModuleDefiner = (moduleId: ModuleID) => void;
+
+ export type ClearFn = () => ModuleList;
+
+ export type RegisterSegmentFn = (
+ segmentId: number,
+ moduleDefiner: ModuleDefiner,
+ moduleIds: Readonly | Nullish
+ ) => void;
+
+ export interface Require extends RequireFn {
+ importDefault: RequireFn;
+ importAll: RequireFn;
+ /** @throws {Error} */
+ context: () => never;
+ /** @throws {Error} */
+ resolveWeak: () => never;
+ unpackModuleId: (moduleId: ModuleID) => {
+ localId: number;
+ segmentId: number;
+ };
+ packModuleId: (value: {
+ localId: number;
+ segmentId: number;
+ }) => ModuleID;
+ }
+}
+
export interface Dispatcher {
_actionHandlers: unknown;
_interceptors?: ((payload: any) => void | boolean)[];
@@ -16,4 +109,3 @@ export interface Dispatcher {
unsubscribe(actionType: string, callback: (payload: any) => void): void;
wait(cb: () => void): void;
}
-
diff --git a/src/lib/metro/utils.ts b/src/lib/metro/utils.ts
new file mode 100644
index 0000000..e6cd4f7
--- /dev/null
+++ b/src/lib/metro/utils.ts
@@ -0,0 +1,11 @@
+import type { Metro } from "@metro/types";
+
+export const modules: Metro.ModuleList = window.modules;
+
+export const metroRequire: Metro.Require = window.__r;
+
+export const define: Metro.DefineFn = window.__d;
+
+export const clear: Metro.ClearFn = window.__c;
+
+export const registerSegment: Metro.RegisterSegmentFn = window.__registerSegment;
diff --git a/src/lib/utils/types.ts b/src/lib/utils/types.ts
index cc257cd..a597341 100644
--- a/src/lib/utils/types.ts
+++ b/src/lib/utils/types.ts
@@ -1,5 +1,7 @@
import { LiteralUnion } from "type-fest";
+export type Nullish = null | undefined;
+
export type OptionalKeys> = LiteralUnion;
export interface Author {
diff --git a/tsconfig.json b/tsconfig.json
index d36f21a..bd56899 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,31 +1,35 @@
{
- "include": ["src"],
- "exclude": ["node_modules"],
+ "exclude": ["dist"],
+
"compilerOptions": {
- "baseUrl": ".",
- "sourceMap": true,
- "module": "ESNext",
- "target": "ESNext",
- "declaration": true,
- "emitDeclarationOnly": true,
- "moduleResolution": "node",
- "esModuleInterop": true,
- "jsx": "react",
"strict": true,
- "stripInternal": true,
- "forceConsistentCasingInFileNames": true,
- "resolveJsonModule": true,
- "skipLibCheck": true,
+
"paths": {
- "@lib/*": ["src/lib/*"],
- "@core/*": ["src/core/*"],
- "@metro/*": ["src/lib/metro/*"],
- "@ui/*": ["src/lib/ui/*"],
- "@types": ["src/lib/utils/types.ts"],
- "bunny-build": ["src/modules.d.ts"]
+ "@core/*": ["./src/core/*"],
+ "@lib": ["./src/lib"],
+ "@lib/*": ["./src/lib/*"],
+ "@metro": ["./src/lib/metro"],
+ "@metro/*": ["./src/lib/metro/*"],
+ "@ui": ["./src/lib/ui"],
+ "@ui/*": ["./src/lib/ui/*"],
+ "@types": ["./src/lib/utils/types.ts"],
+ "bunny-build": ["./src/modules.d.ts"]
},
+
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+
+ "noEmit": true,
+
+ "allowJs": true,
+ "checkJs": true,
+
"plugins": [
{ "transform": "typescript-transform-paths", "afterDeclarations": true }
- ]
+ ],
+
+ "jsx": "react",
+ "target": "esnext"
}
}