Skip to content

Commit

Permalink
refactor+chore: metro & misc. (#29)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
ryan-0324 authored May 5, 2024
1 parent fe58603 commit 6162dfd
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 141 deletions.
6 changes: 5 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -72,4 +76,4 @@
}
]
}
}
}
25 changes: 13 additions & 12 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
}));
Expand Down Expand Up @@ -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"
]
},
});
Expand Down
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
},
Expand All @@ -55,4 +55,4 @@
]
}
}
}
}
178 changes: 94 additions & 84 deletions src/lib/metro/filters.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,77 @@
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<P extends string | symbol> = Record<P, any> & Record<PropertyKey, any>;
export type PropsFinder = <T extends string | symbol>(...props: T[]) => PropIntellisense<T>;
export type PropsFinderAll = <T extends string | symbol>(...props: T[]) => PropIntellisense<T>[];
export type PropIntellisense<P extends PropertyKey> = Record<P, any> & Record<PropertyKey, any>;
export type PropsFinder = <T extends PropertyKey>(...props: T[]) => PropIntellisense<T>;
export type PropsFinderAll = <T extends PropertyKey>(...props: T[]) => PropIntellisense<T>[];

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<Metro.FactoryFn>, 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<string, any> = {};
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];
}
});
}

// 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");
Expand All @@ -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;
};

export const find = filterModules(modules, true);
export const findAll = filterModules(modules);
/**
* 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;
}

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));
Expand Down
5 changes: 3 additions & 2 deletions src/lib/metro/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * as common from "./common";
export * from "./utils";
export {
find,
findAll,
Expand All @@ -10,6 +10,7 @@ export {
findByPropsAll,
findByStoreName,
findByTypeName,
findByTypeNameAll,
findByTypeNameAll
} from "./filters";
export * as filters from "./filters";
export * as common from "./common";
Loading

0 comments on commit 6162dfd

Please sign in to comment.