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)
This commit is contained in:
ryan-0324 2024-05-04 23:33:53 -04:00 • committed by GitHub
parent fe586035e3
commit 6162dfd08b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 264 additions and 141 deletions

View file

@ -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 @@
}
]
}
}
}

View file

@ -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"
]
},
});

View file

@ -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 @@
]
}
}
}
}

View file

@ -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<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];
}
@ -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));

View file

@ -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";

View file

@ -1,4 +1,4 @@
import "@metro/index";
import { metroRequire, modules } from "@metro";
import { LiteralUnion } from "type-fest";
@ -103,25 +103,23 @@ type Keys = LiteralUnion<typeof redesignProps extends Set<infer U> ? U : string,
const redesignPropSource = {} as Record<Keys, any>;
const redesignModule = {} as Record<Keys, any>;
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<string, string | null>();
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) {

View file

@ -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<Record<ModuleID, string>> | undefined;
}
export type InverseDependencyMap = Record<ModuleID, ModuleID[]>;
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<ModuleID, ModuleDefinition | Nullish>;
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<ModuleID[]> | 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;
}

11
src/lib/metro/utils.ts Normal file
View file

@ -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;

View file

@ -1,5 +1,7 @@
import { LiteralUnion } from "type-fest";
export type Nullish = null | undefined;
export type OptionalKeys<T extends Array<string>> = LiteralUnion<T[number], string>;
export interface Author {

View file

@ -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"
}
}