[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:
parent
cfccd5f5b2
commit
1840577fb5
7 changed files with 154 additions and 19 deletions
69
src/def.d.ts
vendored
69
src/def.d.ts
vendored
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`;
|
||||||
|
|
|
@ -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")));
|
||||||
|
|
24
src/lib/storage/backends.ts
Normal file
24
src/lib/storage/backends.ts
Normal 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"),
|
||||||
|
});
|
|
@ -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";
|
|
@ -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: () => {
|
||||||
|
@ -45,4 +49,4 @@ export default async function windowObject(unloads: any[]): Promise<VendettaObje
|
||||||
delete window.vendetta;
|
delete window.vendetta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -56,4 +93,4 @@ export default function Developer() {
|
||||||
</FormSection>
|
</FormSection>
|
||||||
</RN.ScrollView>
|
</RN.ScrollView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue