[Global] Loader config and identity (#17)

* [TS] Add definition for DCDFileManager

* [Storage] Introduce backends

* [Settings] Add loader config

* [TS] Update storage definitions

* [TS] Update loader config and identity types

* [Loader] Expose loader config and identity

* [UI] Actually update UI for the new loader config fields
This commit is contained in:
redstonekasi 2023-02-06 08:48:55 +01:00 committed by GitHub
parent cfccd5f5b2
commit 1840577fb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 154 additions and 19 deletions

69
src/def.d.ts vendored
View file

@ -176,6 +176,42 @@ interface MMKVManager {
clear: () => void; clear: () => void;
} }
interface DCDFileManager {
/**
* @param path **Full** path to file
*/
fileExists: (path: string) => Promise<boolean>;
/**
* Allowed URI schemes on Android: `file://`, `content://` ([See here](https://developer.android.com/reference/android/content/ContentResolver#accepts-the-following-uri-schemes:_3))
*/
getSize: (uri: string) => Promise<boolean>;
/**
* @param path **Full** path to file
* @param encoding Set to `base64` in order to encode response
*/
readFile(path: string, encoding: "base64" | "utf8"): Promise<string>;
/**
* Beware! This function has differing functionality on IOS and Android.
* @param storageDir Either `cache` or `documents`.
* @param path Path in `storageDir`, parents are recursively created.
* @param data The data to write to the file
* @param encoding Set to `base64` if `data` is base64 encoded.
* @returns Promise that resolves to path of the file once it got written
*/
writeFile(storageDir: "cache" | "documents", path: string, data: string, encoding: "base64" | "utf8"): Promise<string>;
getConstants: () => {
/**
* The path the `documents` storage dir (see {@link writeFile}) represents.
*/
DocumentsDirPath: string;
};
/**
* Will apparently cease to exist some time in the future so please use {@link getConstants} instead.
* @deprecated
*/
DocumentsDirPath: string;
}
type Indexable<Type> = { [index: string]: Type } type Indexable<Type> = { [index: string]: Type }
type EmitterEvent = "SET" | "GET" | "DEL"; type EmitterEvent = "SET" | "GET" | "DEL";
@ -200,6 +236,30 @@ interface Emitter {
emit: (event: EmitterEvent, data: EmitterListenerData) => void; emit: (event: EmitterEvent, data: EmitterListenerData) => void;
} }
interface StorageBackend {
get: () => unknown | Promise<unknown>;
set: (data: unknown) => void | Promise<void>;
}
interface LoaderConfig {
customLoadUrl: {
enabled: boolean;
url: string;
};
loadReactDevTools: boolean;
}
interface LoaderIdentity {
name: string;
features: {
loaderConfig?: boolean;
devtools?: {
prop: string;
version: string;
}
}
}
interface VendettaObject { interface VendettaObject {
patcher: { patcher: {
after: typeof _spitroast.after; after: typeof _spitroast.after;
@ -277,11 +337,17 @@ interface VendettaObject {
storage: { storage: {
createProxy: <T>(target: T) => { proxy: T, emitter: Emitter }; createProxy: <T>(target: T) => { proxy: T, emitter: Emitter };
useProxy: <T>(storage: T) => T; useProxy: <T>(storage: T) => T;
createStorage: <T>(storeName: string) => Promise<Awaited<T>>; createStorage: <T>(backend: StorageBackend) => Promise<Awaited<T>>;
wrapSync: <T extends Promise<any>>(store: T) => Awaited<T>; wrapSync: <T extends Promise<any>>(store: T) => Awaited<T>;
awaitSyncWrapper: (store: any) => Promise<void>; awaitSyncWrapper: (store: any) => Promise<void>;
createMMKVBackend: (store: string) => StorageBackend;
createFileBackend: (file: string) => StorageBackend;
}; };
settings: Settings; settings: Settings;
loader: {
identity?: LoaderIdentity;
config: LoaderConfig;
};
logger: Logger; logger: Logger;
version: string; version: string;
unload: () => void; unload: () => void;
@ -299,5 +365,6 @@ declare global {
modules: MetroModules; modules: MetroModules;
vendetta: VendettaObject; vendetta: VendettaObject;
React: typeof _React; React: typeof _React;
__vendetta_loader?: LoaderIdentity;
} }
} }

View file

@ -1,5 +1,5 @@
import { Indexable, PluginManifest, Plugin } from "@types"; import { Indexable, PluginManifest, Plugin } from "@types";
import { awaitSyncWrapper, createStorage, wrapSync } from "@lib/storage"; import { awaitSyncWrapper, createMMKVBackend, createStorage, wrapSync } from "@lib/storage";
import safeFetch from "@utils/safeFetch"; import safeFetch from "@utils/safeFetch";
import logger from "@lib/logger"; import logger from "@lib/logger";
@ -11,7 +11,7 @@ type EvaledPlugin = {
settings: JSX.Element; settings: JSX.Element;
}; };
export const plugins = wrapSync(createStorage<Indexable<Plugin>>("VENDETTA_PLUGINS")); export const plugins = wrapSync(createStorage<Indexable<Plugin>>(createMMKVBackend("VENDETTA_PLUGINS")));
const loadedPlugins: Indexable<EvaledPlugin> = {}; const loadedPlugins: Indexable<EvaledPlugin> = {};
export async function fetchPlugin(id: string) { export async function fetchPlugin(id: string) {
@ -61,7 +61,7 @@ export async function evalPlugin(plugin: Plugin) {
plugin: { plugin: {
manifest: plugin.manifest, manifest: plugin.manifest,
// Wrapping this with wrapSync is NOT an option. // Wrapping this with wrapSync is NOT an option.
storage: await createStorage<Indexable<any>>(plugin.id), storage: await createStorage<Indexable<any>>(createMMKVBackend(plugin.id)),
} }
}; };
const pluginString = `vendetta=>{return ${plugin.js}}\n//# sourceURL=${plugin.id}`; const pluginString = `vendetta=>{return ${plugin.js}}\n//# sourceURL=${plugin.id}`;

View file

@ -1,4 +1,5 @@
import { createStorage, wrapSync } from "@lib/storage"; import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/storage";
import { Settings } from "@types"; import { LoaderConfig, Settings } from "@types";
export default wrapSync(createStorage<Settings>("VENDETTA_SETTINGS")); export default wrapSync(createStorage<Settings>(createMMKVBackend("VENDETTA_SETTINGS")));
export const loaderConfig = wrapSync(createStorage<LoaderConfig>(createFileBackend("vendetta_loader.json")));

View file

@ -0,0 +1,24 @@
import { DCDFileManager, MMKVManager, StorageBackend } from "@types";
import { ReactNative as RN } from "@metro/hoist";
const MMKVManager = RN.NativeModules.MMKVManager as MMKVManager;
const DCDFileManager = RN.NativeModules.DCDFileManager as DCDFileManager;
const filePathFixer: (file: string) => string = RN.Platform.select({
default: (f) => f,
ios: (f) => `Documents/${f}`,
});
export const createMMKVBackend = (store: string): StorageBackend => ({
get: async function() {
return JSON.parse((await MMKVManager.getItem(store)) ?? "{}");
},
set: (data) => MMKVManager.setItem(store, JSON.stringify(data)),
});
export const createFileBackend = (file: string): StorageBackend => ({
get: async function() {
return JSON.parse((await DCDFileManager.readFile(`${DCDFileManager.getConstants().DocumentsDirPath}/${file}`, "utf8")) ?? "{}");
},
set: (data) => void DCDFileManager.writeFile("documents", filePathFixer(file), JSON.stringify(data), "utf8"),
});

View file

@ -1,6 +1,6 @@
import { Emitter, MMKVManager } from "@types"; import { Emitter, MMKVManager, StorageBackend } from "@types";
import { ReactNative as RN } from "@metro/hoist"; import { ReactNative as RN } from "@metro/hoist";
import createEmitter from "./emitter"; import createEmitter from "../emitter";
const MMKVManager = RN.NativeModules.MMKVManager as MMKVManager; const MMKVManager = RN.NativeModules.MMKVManager as MMKVManager;
@ -79,11 +79,11 @@ export function useProxy<T>(storage: T): T {
return storage; return storage;
} }
export async function createStorage<T>(storeName: string): Promise<Awaited<T>> { export async function createStorage<T>(backend: StorageBackend): Promise<Awaited<T>> {
const data = JSON.parse((await MMKVManager.getItem(storeName)) ?? "{}"); const data = await backend.get();
const { proxy, emitter } = createProxy(data); const { proxy, emitter } = createProxy(data);
const handler = () => MMKVManager.setItem(storeName, JSON.stringify(proxy)); const handler = () => backend.set(proxy);
emitter.on("SET", handler); emitter.on("SET", handler);
emitter.on("DEL", handler); emitter.on("DEL", handler);
@ -115,3 +115,5 @@ export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {
} }
export const awaitSyncWrapper = (store: any) => new Promise<void>((res) => store[syncAwaitSymbol](res)); export const awaitSyncWrapper = (store: any) => new Promise<void>((res) => store[syncAwaitSymbol](res));
export * from "./backends";

View file

@ -1,7 +1,7 @@
import { VendettaObject } from "@types"; import { VendettaObject } from "@types";
import patcher from "@lib/patcher"; import patcher from "@lib/patcher";
import logger from "@lib/logger"; import logger from "@lib/logger";
import settings from "@lib/settings"; import settings, { loaderConfig } from "@lib/settings";
import * as constants from "@lib/constants"; import * as constants from "@lib/constants";
import * as debug from "@lib/debug"; import * as debug from "@lib/debug";
import * as plugins from "@lib/plugins"; import * as plugins from "@lib/plugins";
@ -37,6 +37,10 @@ export default async function windowObject(unloads: any[]): Promise<VendettaObje
commands: without(commands, "patchCommands"), commands: without(commands, "patchCommands"),
storage, storage,
settings, settings,
loader: {
identity: window.__vendetta_loader,
config: loaderConfig,
},
logger, logger,
version: debug.versionHash, version: debug.versionHash,
unload: () => { unload: () => {

View file

@ -1,17 +1,28 @@
import { ReactNative as RN, NavigationNative } from "@metro/common"; import { ReactNative as RN, NavigationNative, stylesheet, constants } from "@metro/common";
import { Forms } from "@ui/components"; import { Forms, General } from "@ui/components";
import { getAssetIDByName } from "@ui/assets"; import { getAssetIDByName } from "@ui/assets";
import { showToast } from "@ui/toasts"; import { showToast } from "@ui/toasts";
import { connectToDebugger } from "@lib/debug"; import { connectToDebugger } from "@lib/debug";
import { useProxy } from "@lib/storage"; import { useProxy } from "@lib/storage";
import settings from "@lib/settings"; import settings, { loaderConfig } from "@lib/settings";
import logger from "@lib/logger"; import logger from "@lib/logger";
const { FormSection, FormRow, FormInput, FormDivider } = Forms; const { FormSection, FormRow, FormSwitchRow, FormInput, FormDivider } = Forms;
const { Text } = General;
const styles = stylesheet.createThemedStyleSheet({
code: {
fontFamily: constants.Fonts.CODE_SEMIBOLD,
includeFontPadding: false,
fontSize: 12,
}
});
export default function Developer() { export default function Developer() {
const navigation = NavigationNative.useNavigation(); const navigation = NavigationNative.useNavigation();
useProxy(settings); useProxy(settings);
useProxy(loaderConfig);
return ( return (
<RN.ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 38 }}> <RN.ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 38 }}>
@ -46,6 +57,32 @@ export default function Developer() {
}} }}
/>} />}
</FormSection> </FormSection>
{window.__vendetta_loader?.features.loaderConfig && <FormSection title="Loader config">
<FormSwitchRow
label="Load from custom url"
subLabel={"Load Vendetta from a custom endpoint."}
leading={<FormRow.Icon source={getAssetIDByName("copy")} />}
value={loaderConfig.customLoadUrl.enabled}
onValueChange={(v: boolean) => {
loaderConfig.customLoadUrl.enabled = v;
}}
/>
{loaderConfig.customLoadUrl.enabled && <FormInput
value={loaderConfig.customLoadUrl.url}
onChange={(v: string) => loaderConfig.customLoadUrl.url = v}
placeholder="http://localhost:4040/vendetta.js"
title="VENDETTA URL"
/>}
{window.__vendetta_loader.features.devtools && <FormSwitchRow
label="Load React DevTools"
subLabel={`Version: ${window.__vendetta_loader.features.devtools.version}`}
leading={<FormRow.Icon source={getAssetIDByName("ic_badge_staff")} />}
value={loaderConfig.loadReactDevTools}
onValueChange={(v: boolean) => {
loaderConfig.loadReactDevTools = v;
}}
/>}
</FormSection>}
<FormSection title="Other"> <FormSection title="Other">
<FormRow <FormRow
label="Asset Browser" label="Asset Browser"