Merge branch 'dev'
This commit is contained in:
commit
945acc1738
27 changed files with 2672 additions and 1529 deletions
|
|
@ -1,7 +1,3 @@
|
|||
{
|
||||
"languages": {
|
||||
"TypeScript": {
|
||||
"format_on_save": "off"
|
||||
}
|
||||
}
|
||||
"format_on_save": "off",
|
||||
}
|
||||
|
|
|
|||
52
package.json
52
package.json
|
|
@ -15,43 +15,47 @@
|
|||
"devDependencies": {
|
||||
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/exec": "^6.0.3",
|
||||
"@semantic-release/exec": "^7.1.0",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"semantic-release": "^24.2.7",
|
||||
"semantic-release": "^25.0.2",
|
||||
"@swc/core": "^1.13.1",
|
||||
"@swc/helpers": "^0.5.17",
|
||||
"@types/node": "^20.19.9",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/yargs-parser": "^21.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@typescript-eslint/typescript-estree": "^7.18.0",
|
||||
"chalk": "^5.4.1",
|
||||
"esbuild": "^0.20.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"@typescript-eslint/typescript-estree": "^8.50.1",
|
||||
"chalk": "^5.6.2",
|
||||
"esbuild": "^0.27.2",
|
||||
"esbuild-plugin-globals": "^0.2.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-import-alias": "^1.2.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^3.2.0",
|
||||
"typescript": "^5.8.3",
|
||||
"yargs-parser": "^21.1.1"
|
||||
"eslint-plugin-unused-imports": "^4.3.0",
|
||||
"typescript": "^5.9.3",
|
||||
"yargs-parser": "^22.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gullerya/object-observer": "^6.1.4",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@types/chroma-js": "~2.4.5",
|
||||
"@types/lodash": "~4.17.20",
|
||||
"@types/react": "19.1.8",
|
||||
"es-toolkit": "^1.39.7",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@types/chroma-js": "~3.1.2",
|
||||
"@types/lodash": "~4.17.21",
|
||||
"@types/react": "19.2.7",
|
||||
"es-toolkit": "^1.43.0",
|
||||
"fuzzysort": "^3.1.0",
|
||||
"intl-messageformat": "^10.7.16",
|
||||
"moment": "2.22.2",
|
||||
"react-native": "0.78.0",
|
||||
"react-native-reanimated": "^3.18.0",
|
||||
"spitroast": "^1.4.4",
|
||||
"type-fest": "^4.41.0"
|
||||
"intl-messageformat": "^11.0.5",
|
||||
"moment": "2.30.1",
|
||||
"react-devtools": "^7.0.1",
|
||||
"react-native": "0.83.1",
|
||||
"react-native-reanimated": "^4.2.1",
|
||||
"react-native-webview": "^13.16.0",
|
||||
"spitroast": "^2.1.6",
|
||||
"type-fest": "^5.3.1"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@swc/core"
|
||||
"@swc/core",
|
||||
"electron",
|
||||
"esbuild"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ module.exports = {
|
|||
"util": findByPropsLazy("inspect", "isNullOrUndefined"),
|
||||
"moment": findByPropsLazy("isMoment"),
|
||||
"chroma-js": findByPropsLazy("brewer"),
|
||||
"lodash": findByPropsLazy("forEachRight")
|
||||
// "@shopify/react-native-skia": findByPropsLazy("useFont")
|
||||
"lodash": findByPropsLazy("forEachRight"),
|
||||
"react-native-webview": findByPropsLazy("WebView")
|
||||
};
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ export default defineCorePlugin({
|
|||
|
||||
// Initialize sound settings
|
||||
settings.startupSound = settings.startupSound || {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
probability: 2,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const useStyles = createStyles({
|
|||
},
|
||||
headerLabel: {
|
||||
...TextStyleSheet["heading-md/semibold"],
|
||||
color: semanticColors.HEADER_PRIMARY,
|
||||
color: semanticColors.MOBILE_TEXT_HEADING_PRIMARY,
|
||||
},
|
||||
headerSubtitle: {
|
||||
...TextStyleSheet["text-md/semibold"],
|
||||
|
|
@ -60,7 +60,7 @@ const useStyles = createStyles({
|
|||
gap: 5,
|
||||
},
|
||||
iconStyle: {
|
||||
tintColor: semanticColors.LOGO_PRIMARY,
|
||||
tintColor: semanticColors.INTERACTIVE_ICON_DEFAULT,
|
||||
opacity: 0.2,
|
||||
height: 64,
|
||||
width: 64,
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ export default function AddonPage<T extends object>({
|
|||
>
|
||||
<View style={{ gap: 8, alignItems: "center" }}>
|
||||
<Image source={findAssetId("empty_quick_switcher")!} />
|
||||
<Text variant="text-lg/semibold" color="text-normal">
|
||||
<Text variant="text-lg/semibold" color="text-default">
|
||||
Oops! Nothing to see here… yet!
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -303,7 +303,7 @@ export default function AddonPage<T extends object>({
|
|||
ListEmptyComponent={() => (
|
||||
<View style={{ gap: 12, padding: 12, alignItems: "center" }}>
|
||||
<Image source={findAssetId("devices_not_found")!} />
|
||||
<Text variant="text-lg/semibold" color="text-normal">
|
||||
<Text variant="text-lg/semibold" color="text-default">
|
||||
Hmmm... could not find that!
|
||||
</Text>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import { CheckState, useFileExists } from "@core/ui/hooks/useFS";
|
|||
import AssetBrowser from "@core/ui/settings/pages/Developer/AssetBrowser";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { connectToDebugger } from "@lib/api/debug";
|
||||
import {
|
||||
connectToDebugger,
|
||||
disconnectFromDebugger,
|
||||
isConnectedToDebugger,
|
||||
} from "@lib/api/debug";
|
||||
import {
|
||||
getReactDevToolsProp,
|
||||
getReactDevToolsVersion,
|
||||
|
|
@ -31,6 +35,7 @@ import { createStyles, TextStyleSheet } from "@ui/styles";
|
|||
import { NativeModules } from "react-native";
|
||||
import { ScrollView, StyleSheet } from "react-native";
|
||||
import { showToast } from "@ui/toasts";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const { hideActionSheet } = lazyDestructure(() =>
|
||||
findByProps("openLazy", "hideActionSheet"),
|
||||
|
|
@ -45,8 +50,8 @@ const { AlertModal, AlertActionButton } = lazyDestructure(() =>
|
|||
findByProps("AlertModal", "AlertActions"),
|
||||
);
|
||||
|
||||
// todo: This link isnt minified, but it probably should be
|
||||
const RDT_EMBED_LINK = "https://codeberg.org/raincord/Devtools/raw/branch/main/reactDevtools.js";
|
||||
const RDT_EMBED_LINK =
|
||||
"https://codeberg.org/raincord/raindevtools/raw/branch/dev/dist/index.bundle";
|
||||
|
||||
const useStyles = createStyles({
|
||||
leadingText: {
|
||||
|
|
@ -64,6 +69,9 @@ const useStyles = createStyles({
|
|||
|
||||
export default function Developer() {
|
||||
const [rdtFileExists, fs] = useFileExists("preloads/reactDevtools.js");
|
||||
const [isDebuggerConnected, setIsDebuggerConnected] = useState(
|
||||
isConnectedToDebugger(),
|
||||
);
|
||||
|
||||
const styles = useStyles();
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
|
|
@ -71,6 +79,23 @@ export default function Developer() {
|
|||
useProxy(settings);
|
||||
useProxy(loaderConfig);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setIsDebuggerConnected(isConnectedToDebugger());
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const handleDebuggerConnect = () => {
|
||||
if (isDebuggerConnected) {
|
||||
disconnectFromDebugger();
|
||||
setIsDebuggerConnected(false);
|
||||
} else {
|
||||
connectToDebugger(settings.debuggerUrl);
|
||||
setTimeout(() => setIsDebuggerConnected(isConnectedToDebugger()), 100);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ScrollView
|
||||
|
|
@ -250,6 +275,11 @@ export default function Developer() {
|
|||
fs.downloadFile(
|
||||
RDT_EMBED_LINK,
|
||||
"preloads/reactDevtools.js",
|
||||
).then(() =>
|
||||
showToast(
|
||||
"Successfully installed! A reload is required",
|
||||
findAssetId("DownloadIcon"),
|
||||
),
|
||||
);
|
||||
} else if (rdtFileExists === CheckState.TRUE) {
|
||||
fs.removeFile("preloads/reactDevtools.js");
|
||||
|
|
@ -257,7 +287,7 @@ export default function Developer() {
|
|||
}}
|
||||
icon={findAssetId(
|
||||
rdtFileExists === CheckState.TRUE
|
||||
? "ic_message_delete"
|
||||
? "TrashIcon"
|
||||
: "DownloadIcon",
|
||||
)}
|
||||
style={{ marginLeft: 8 }}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,89 @@ import { showConfirmationAlert } from "@core/vendetta/alerts";
|
|||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { FontDefinition, fonts, selectFont } from "@lib/addons/fonts";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { BundleUpdaterManager } from "@lib/api/native/modules";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
import { Button, Card, IconButton, Text } from "@metro/common/components";
|
||||
import { View } from "react-native";
|
||||
import { createStyles, TextStyleSheet } from "@lib/ui/styles";
|
||||
import { NavigationNative, tokens } from "@metro/common";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
IconButton,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@metro/common/components";
|
||||
import { findByProps } from "@metro";
|
||||
import { useMemo, useState } from "react";
|
||||
import { PixelRatio, View } from "react-native";
|
||||
import { WebView } from "react-native-webview";
|
||||
import previewHtml from "./preview.html";
|
||||
|
||||
import FontEditor from "./FontEditor";
|
||||
|
||||
const { useToken } = lazyDestructure(() => findByProps("useToken"));
|
||||
|
||||
const useStyles = createStyles({
|
||||
full: {
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
});
|
||||
|
||||
function FontPreview({ font }: { font: FontDefinition }) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const styles = useStyles();
|
||||
|
||||
const TEXT_DEFAULT = useToken(tokens.colors.TEXT_DEFAULT);
|
||||
const { fontFamily: fontFamilyList, fontSize } =
|
||||
TextStyleSheet["text-md/medium"];
|
||||
const fontFamily = fontFamilyList!.split(/,/g)[0];
|
||||
|
||||
const props = useMemo(
|
||||
() => ({
|
||||
family: font.main[fontFamily],
|
||||
size: fontSize! * PixelRatio.getFontScale(),
|
||||
color: TEXT_DEFAULT,
|
||||
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
}),
|
||||
[font.main, fontFamily, fontSize, TEXT_DEFAULT],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={{ width: "100%", height: 64 }}>
|
||||
<WebView
|
||||
onMessage={() => setLoaded(true)}
|
||||
source={{
|
||||
html: previewHtml.replace("$$props", JSON.stringify(props)),
|
||||
}}
|
||||
javaScriptEnabled
|
||||
scrollEnabled={false}
|
||||
overScrollMode="never"
|
||||
showsHorizontalScrollIndicator={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
pointerEvents="none"
|
||||
style={[
|
||||
styles.full,
|
||||
{ backgroundColor: "transparent", opacity: Number(loaded) },
|
||||
]}
|
||||
/>
|
||||
{!loaded && (
|
||||
<View
|
||||
style={[
|
||||
styles.full,
|
||||
{ justifyContent: "center", alignItems: "center" },
|
||||
]}
|
||||
>
|
||||
<Text color="text-muted" variant="heading-lg/semibold">
|
||||
Loading...
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FontCard({ item: font }: CardWrapper<FontDefinition>) {
|
||||
useProxy(fonts);
|
||||
|
||||
|
|
@ -18,50 +94,51 @@ export default function FontCard({ item: font }: CardWrapper<FontDefinition>) {
|
|||
const selected = fonts.__selected === font.name;
|
||||
|
||||
return (
|
||||
<Card style={{ padding: 18, borderRadius: 16, backgroundColor: "#23272a" }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text variant="heading-lg/bold">{font.name}</Text>
|
||||
</View>
|
||||
|
||||
<Card>
|
||||
<Stack spacing={16}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={selected ? "secondary" : "primary"}
|
||||
text={selected ? "Unapply" : "Apply"}
|
||||
style={{ minWidth: 80, marginRight: 8 }}
|
||||
onPress={async () => {
|
||||
await selectFont(selected ? null : font.name);
|
||||
showConfirmationAlert({
|
||||
title: Strings.HOLD_UP,
|
||||
content: "Reload Discord to apply changes?",
|
||||
confirmText: Strings.RELOAD,
|
||||
cancelText: Strings.CANCEL,
|
||||
confirmColor: "red",
|
||||
onConfirm: BundleUpdaterManager.reload,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
navigation.push("BUNNY_CUSTOM_PAGE", {
|
||||
title: "Edit Font",
|
||||
render: () => <FontEditor name={font.name} />,
|
||||
});
|
||||
}}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={selected}
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
/>
|
||||
<View>
|
||||
<Text variant="heading-lg/semibold">{font.name}</Text>
|
||||
{/* TODO: Text wrapping doesn't work well */}
|
||||
{/* <Text color="text-muted" variant="text-sm/semibold">
|
||||
{font.description}
|
||||
</Text> */}
|
||||
</View>
|
||||
<View style={{ marginLeft: "auto" }}>
|
||||
<Stack spacing={12} direction="horizontal">
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
navigation.push("BUNNY_CUSTOM_PAGE", {
|
||||
title: "Edit Font",
|
||||
render: () => <FontEditor name={font.name} />,
|
||||
});
|
||||
}}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={selected}
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={selected ? "secondary" : "primary"}
|
||||
text={selected ? "Unapply" : "Apply"}
|
||||
onPress={async () => {
|
||||
await selectFont(selected ? null : font.name);
|
||||
showConfirmationAlert({
|
||||
title: Strings.HOLD_UP,
|
||||
content: "Reload Discord to apply changes?",
|
||||
confirmText: Strings.RELOAD,
|
||||
cancelText: Strings.CANCEL,
|
||||
confirmColor: "red",
|
||||
onConfirm: BundleUpdaterManager.reload,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<FontPreview font={font} />
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ export default function FontEditor(props: {
|
|||
|
||||
return <TableRow
|
||||
label={name}
|
||||
subLabel={error ? <Text variant='text-xs/medium' color='text-danger'>{error.message}</Text> : url}
|
||||
subLabel={error ? <Text variant='text-xs/medium' color='text-feedback-critical'>{error.message}</Text> : url}
|
||||
trailing={<Stack spacing={8} direction="horizontal">
|
||||
<IconButton
|
||||
size="sm"
|
||||
|
|
@ -363,7 +363,7 @@ export default function FontEditor(props: {
|
|||
})}
|
||||
<TableRow label={<NewEntryRow fontName={props.name} fontEntry={fontEntries} />} />
|
||||
</TableRowGroup>
|
||||
{errors && <Text variant='text-sm/medium' color='text-danger'>Some font entries cannot be imported. Please modify the entries and try again.</Text>}
|
||||
{errors && <Text variant='text-sm/medium' color='text-feedback-critical'>Some font entries cannot be imported. Please modify the entries and try again.</Text>}
|
||||
<View style={{ flexDirection: "row", justifyContent: "flex-end", bottom: 0, left: 0 }}>
|
||||
<Button
|
||||
size="lg"
|
||||
|
|
|
|||
20
src/core/ui/settings/pages/Fonts/preview.html
Normal file
20
src/core/ui/settings/pages/Fonts/preview.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<head>
|
||||
<meta name="viewport" content="initial-scale=1.0" />
|
||||
</head>
|
||||
<body style="overflow: hidden; padding: 0; margin: 0"></body>
|
||||
<script>
|
||||
const props = $$props;
|
||||
|
||||
new FontFace("Preview", `url(${props.family})`).load().then((font) => {
|
||||
document.fonts.add(font);
|
||||
|
||||
const p = document.createElement("p");
|
||||
p.textContent = props.text;
|
||||
p.style.fontFamily = font.family;
|
||||
p.style.fontSize = `${props.size}px`;
|
||||
p.style.color = props.color;
|
||||
|
||||
document.body.appendChild(p);
|
||||
ReactNativeWebView.postMessage(true);
|
||||
});
|
||||
</script>
|
||||
|
|
@ -79,7 +79,7 @@ export default function General() {
|
|||
<Text
|
||||
variant="text-md/semibold"
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
color: "text-default",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ const Actions = () => {
|
|||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
icon={findAssetId("SettingsIcon")}
|
||||
disabled={!plugin.getPluginSettingsComponent()}
|
||||
onPress={() =>
|
||||
navigation.push("SHIGGYCORD_CUSTOM_PAGE", {
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ export default function PluginInfoActionSheet({
|
|||
label="Configure"
|
||||
variant="secondary"
|
||||
disabled={!plugin.getPluginSettingsComponent()}
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
icon={findAssetId("SettingsIcon")}
|
||||
onPress={() => {
|
||||
navigation.push("SHIGGYCORD_CUSTOM_PAGE", {
|
||||
title: plugin.name,
|
||||
|
|
@ -236,7 +236,7 @@ export default function PluginInfoActionSheet({
|
|||
variant="text-md/semibold"
|
||||
style={{
|
||||
marginBottom: 4,
|
||||
color: semanticColors.HEADER_PRIMARY,
|
||||
color: "text-strong",
|
||||
}}
|
||||
>
|
||||
Description
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export default function PluginInfoActionSheet({
|
|||
size="md"
|
||||
text="Configure"
|
||||
variant="secondary"
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
icon={findAssetId("SettingsIcon")}
|
||||
onPress={() => {
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
navigation.push("SHIGGYCORD_CUSTOM_PAGE", {
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ export default function ThemeInfoActionSheet({
|
|||
variant="text-md/semibold"
|
||||
style={{
|
||||
marginBottom: 4,
|
||||
color: semanticColors.HEADER_PRIMARY,
|
||||
color: semanticColors.MOBILE_TEXT_HEADING_PRIMARY,
|
||||
}}
|
||||
>
|
||||
Description
|
||||
|
|
|
|||
|
|
@ -140,10 +140,21 @@ export const initVendettaObject = (): any => {
|
|||
FluxDispatcher: common.FluxDispatcher,
|
||||
React: common.React,
|
||||
ReactNative: common.ReactNative,
|
||||
moment: require("moment"),
|
||||
chroma: require("chroma-js"),
|
||||
lodash: require("lodash"),
|
||||
util: require("util"),
|
||||
get moment() {
|
||||
// Lazy-require heavy libs to avoid pulling them into the startup bundle.
|
||||
// Consumers access `window.vendetta.common.moment` and this getter will
|
||||
// require the module on first access.
|
||||
return require("moment");
|
||||
},
|
||||
get chroma() {
|
||||
return require("chroma-js");
|
||||
},
|
||||
get lodash() {
|
||||
return require("lodash");
|
||||
},
|
||||
get util() {
|
||||
return require("util");
|
||||
},
|
||||
},
|
||||
},
|
||||
constants: {
|
||||
|
|
|
|||
23
src/global.d.ts
vendored
23
src/global.d.ts
vendored
|
|
@ -1,18 +1,17 @@
|
|||
declare global {
|
||||
type React = typeof import("react");
|
||||
var SkiaApi: typeof import("@shopify/react-native-skia").Skia;
|
||||
type React = typeof import("react");
|
||||
|
||||
// ReactNative/Hermes globals
|
||||
var globalEvalWithSourceUrl: (script: string, sourceURL: string) => any;
|
||||
var nativePerformanceNow: typeof performance.now;
|
||||
var nativeModuleProxy: Record<string, any>;
|
||||
var __turboModuleProxy: (name: string) => any;
|
||||
// ReactNative/Hermes globals
|
||||
var globalEvalWithSourceUrl: (script: string, sourceURL: string) => any;
|
||||
var nativePerformanceNow: typeof performance.now;
|
||||
var nativeModuleProxy: Record<string, any>;
|
||||
var __turboModuleProxy: (name: string) => any;
|
||||
|
||||
interface Window {
|
||||
[key: string]: any;
|
||||
vendetta: any;
|
||||
bunny: typeof import("@lib");
|
||||
}
|
||||
interface Window {
|
||||
[key: string]: any;
|
||||
vendetta: any;
|
||||
bunny: typeof import("@lib");
|
||||
}
|
||||
}
|
||||
|
||||
export { };
|
||||
|
|
|
|||
37
src/index.ts
37
src/index.ts
|
|
@ -19,30 +19,35 @@ import { getDebugInfo, initDebugger } from "@lib/api/debug";
|
|||
// imported when needed (to avoid bundling it permanently) and removed after use.
|
||||
|
||||
import * as lib from "./lib";
|
||||
import { timings } from "@lib/utils/timings";
|
||||
|
||||
/**
|
||||
* Start sequence split into critical (UI) and deferred (network/plugin) work.
|
||||
* The goal is to get the UI ready quickly and run heavy tasks after interactions.
|
||||
*/
|
||||
export default async () => {
|
||||
// Critical initializers that are required for the app's UI and basic features.
|
||||
const criticalInits = [
|
||||
initThemes(),
|
||||
injectFluxInterceptor(),
|
||||
patchSettings(),
|
||||
patchLogHook(),
|
||||
patchCommands(),
|
||||
patchJsx(),
|
||||
patchErrorBoundary(),
|
||||
initVendettaObject(),
|
||||
initFetchI18nStrings(),
|
||||
initSettings(),
|
||||
initFixes(),
|
||||
initDebugger(),
|
||||
// Wrap critical initializers as named functions so we can instrument each.
|
||||
const criticalInitFns: Array<[string, () => Promise<any>]> = [
|
||||
["initThemes", () => initThemes()],
|
||||
["injectFluxInterceptor", () => injectFluxInterceptor()],
|
||||
["patchSettings", () => patchSettings()],
|
||||
["patchLogHook", () => patchLogHook()],
|
||||
["patchCommands", () => patchCommands()],
|
||||
["patchJsx", () => patchJsx()],
|
||||
["patchErrorBoundary", () => patchErrorBoundary()],
|
||||
["initVendettaObject", () => initVendettaObject()],
|
||||
["initFetchI18nStrings", () => initFetchI18nStrings()],
|
||||
["initSettings", () => initSettings()],
|
||||
["initFixes", () => initFixes()],
|
||||
["initDebugger", () => initDebugger()],
|
||||
];
|
||||
|
||||
// Run critical inits and collect unpatchers/cleanup handlers.
|
||||
await Promise.all(criticalInits)
|
||||
// Run critical inits with timing instrumentation and collect unpatchers/cleanup handlers.
|
||||
await Promise.all(
|
||||
criticalInitFns.map(([name, fn]) =>
|
||||
timings.measureAsync(`critical:${name}`, async () => fn()),
|
||||
),
|
||||
)
|
||||
.then((u) => u.forEach((f) => f && lib.unload.push(f)))
|
||||
.catch((e) => {
|
||||
// Log but don't abort — critical inits failing should be visible in logs.
|
||||
|
|
|
|||
|
|
@ -14,18 +14,49 @@ export interface Asset {
|
|||
// Cache common usage
|
||||
const _nameToAssetCache = {} as Record<string, Asset>;
|
||||
|
||||
// Simple cached asset list with TTL to avoid iterating the flagsIndex too often.
|
||||
// This is a small in-memory cache to improve repeated asset lookups during UI rendering.
|
||||
let _cachedAssetList: Asset[] | null = null;
|
||||
let _cachedAssetListTs = 0;
|
||||
const ASSET_CACHE_TTL = 2500; // milliseconds
|
||||
|
||||
/**
|
||||
* Invalidate the internal asset caches so the next iterateAssets() call will
|
||||
* re-run through the metro flags index and refresh the cache.
|
||||
*/
|
||||
export function invalidateAssetCache() {
|
||||
_cachedAssetList = null;
|
||||
_cachedAssetListTs = 0;
|
||||
for (const k in _nameToAssetCache) delete _nameToAssetCache[k];
|
||||
}
|
||||
|
||||
export function* iterateAssets() {
|
||||
// Return cached list when fresh
|
||||
const now = Date.now();
|
||||
if (_cachedAssetList && now - _cachedAssetListTs < ASSET_CACHE_TTL) {
|
||||
for (const asset of _cachedAssetList) yield asset;
|
||||
return;
|
||||
}
|
||||
|
||||
const { flagsIndex } = getMetroCache();
|
||||
const yielded = new Set<number>();
|
||||
const list: Asset[] = [];
|
||||
|
||||
for (const id in flagsIndex) {
|
||||
if (flagsIndex[id] & ModuleFlags.ASSET) {
|
||||
const assetId = requireModule(Number(id));
|
||||
if (typeof assetId !== "number" || yielded.has(assetId)) continue;
|
||||
yield getAssetById(assetId);
|
||||
const asset = getAssetById(assetId);
|
||||
if (!asset) continue;
|
||||
list.push(asset);
|
||||
yielded.add(assetId);
|
||||
yield asset;
|
||||
}
|
||||
}
|
||||
|
||||
// Update cache
|
||||
_cachedAssetList = list;
|
||||
_cachedAssetListTs = now;
|
||||
}
|
||||
|
||||
// Apply additional properties for convenience
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import { getThemeFromLoader, selectTheme, themes } from "@lib/addons/themes";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { getLoaderName, getLoaderVersion, isThemeSupported, getReactDevToolsProp } from "@lib/api/native/loader";
|
||||
import { getLoaderName, getLoaderVersion, isThemeSupported, getReactDevToolsProp, isReactDevToolsPreloaded } from "@lib/api/native/loader";
|
||||
import { BundleUpdaterManager, NativeClientInfoModule, NativeDeviceModule } from "@lib/api/native/modules";
|
||||
import { after } from "@lib/api/patcher";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { logger } from "@lib/utils/logger";
|
||||
import { showToast } from "@ui/toasts";
|
||||
import { version } from "bunny-build-info";
|
||||
import { Platform, type PlatformConstants } from "react-native";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Platform, type PlatformConstants, StyleSheet } from "react-native";
|
||||
|
||||
export interface RNConstants extends PlatformConstants {
|
||||
// Android
|
||||
|
|
@ -44,38 +43,291 @@ export async function toggleSafeMode() {
|
|||
setTimeout(BundleUpdaterManager.reload, 400);
|
||||
}
|
||||
|
||||
let socket: WebSocket;
|
||||
// --- Debugger State & Logic ---
|
||||
let socket: WebSocket | undefined;
|
||||
let originalConsoleLog: any;
|
||||
let originalConsoleError: any;
|
||||
let originalConsoleWarn: any;
|
||||
let originalLoggerLog: any;
|
||||
let originalLoggerError: any;
|
||||
let originalLoggerWarn: any;
|
||||
|
||||
const VERSION = 1;
|
||||
|
||||
enum MessageType {
|
||||
Hello = 'hello',
|
||||
Hi = 'hi',
|
||||
Log = 'log',
|
||||
Run = 'run',
|
||||
}
|
||||
|
||||
enum LogLevel {
|
||||
Debug = 'debug',
|
||||
Default = 'default',
|
||||
Warn = 'warn',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
interface LogMessage {
|
||||
type: MessageType.Log;
|
||||
data: {
|
||||
level: LogLevel;
|
||||
message: any[];
|
||||
};
|
||||
}
|
||||
|
||||
interface HelloMessage {
|
||||
type: MessageType.Hello;
|
||||
data: {
|
||||
version: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface RunMessage {
|
||||
type: MessageType.Run;
|
||||
data: {
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
function serializeMessage(msg: any): string {
|
||||
return JSON.stringify(msg);
|
||||
}
|
||||
|
||||
function sendLog(level: LogLevel, ...args: any[]) {
|
||||
if (socket?.readyState === WebSocket.OPEN) {
|
||||
const message: LogMessage = {
|
||||
type: MessageType.Log,
|
||||
data: {
|
||||
level,
|
||||
message: args
|
||||
}
|
||||
};
|
||||
socket.send(serializeMessage(message));
|
||||
}
|
||||
}
|
||||
|
||||
function patchConsoleAndLogger() {
|
||||
originalConsoleLog = console.log;
|
||||
console.log = function(...args: any[]) {
|
||||
originalConsoleLog.apply(console, args);
|
||||
sendLog(LogLevel.Default, ...args);
|
||||
};
|
||||
|
||||
originalConsoleError = console.error;
|
||||
console.error = function(...args: any[]) {
|
||||
originalConsoleError.apply(console, args);
|
||||
sendLog(LogLevel.Error, ...args);
|
||||
};
|
||||
|
||||
originalConsoleWarn = console.warn;
|
||||
console.warn = function(...args: any[]) {
|
||||
originalConsoleWarn.apply(console, args);
|
||||
sendLog(LogLevel.Warn, ...args);
|
||||
};
|
||||
|
||||
if (logger) {
|
||||
originalLoggerLog = logger.log;
|
||||
logger.log = function(...args: any[]) {
|
||||
originalLoggerLog.apply(logger, args);
|
||||
sendLog(LogLevel.Default, ...args);
|
||||
};
|
||||
|
||||
originalLoggerError = logger.error;
|
||||
logger.error = function(...args: any[]) {
|
||||
originalLoggerError.apply(logger, args);
|
||||
sendLog(LogLevel.Error, ...args);
|
||||
};
|
||||
|
||||
originalLoggerWarn = logger.warn;
|
||||
logger.warn = function(...args: any[]) {
|
||||
originalLoggerWarn.apply(logger, args);
|
||||
sendLog(LogLevel.Warn, ...args);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function unpatchConsoleAndLogger() {
|
||||
if (originalConsoleLog) {
|
||||
console.log = originalConsoleLog;
|
||||
originalConsoleLog = undefined;
|
||||
}
|
||||
if (originalConsoleError) {
|
||||
console.error = originalConsoleError;
|
||||
originalConsoleError = undefined;
|
||||
}
|
||||
if (originalConsoleWarn) {
|
||||
console.warn = originalConsoleWarn;
|
||||
originalConsoleWarn = undefined;
|
||||
}
|
||||
|
||||
if (logger) {
|
||||
if (originalLoggerLog) {
|
||||
logger.log = originalLoggerLog;
|
||||
originalLoggerLog = undefined;
|
||||
}
|
||||
if (originalLoggerError) {
|
||||
logger.error = originalLoggerError;
|
||||
originalLoggerError = undefined;
|
||||
}
|
||||
if (originalLoggerWarn) {
|
||||
logger.warn = originalLoggerWarn;
|
||||
originalLoggerWarn = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function connectToDebugger(url: string) {
|
||||
if (socket !== undefined && socket.readyState !== WebSocket.CLOSED) socket.close();
|
||||
if (socket !== undefined && socket.readyState !== WebSocket.CLOSED) {
|
||||
unpatchConsoleAndLogger();
|
||||
socket.close();
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
showToast("Invalid debugger URL!", findAssetId("Small"));
|
||||
return;
|
||||
}
|
||||
|
||||
socket = new WebSocket(`ws://${url}`);
|
||||
try {
|
||||
socket = new WebSocket(`ws://${url}`);
|
||||
|
||||
socket.addEventListener("open", () => showToast("Connected to debugger.", findAssetId("Check")));
|
||||
socket.addEventListener("message", (message: any) => {
|
||||
try {
|
||||
(0, eval)(message.data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
socket.addEventListener("open", () => {
|
||||
showToast("Connected to debugger.", findAssetId("Check"));
|
||||
|
||||
const hello: HelloMessage = {
|
||||
type: MessageType.Hello,
|
||||
data: {
|
||||
version: VERSION
|
||||
}
|
||||
};
|
||||
socket?.send(serializeMessage(hello));
|
||||
|
||||
socket.addEventListener("error", (err: any) => {
|
||||
console.log(`Debugger error: ${err.message}`);
|
||||
showToast("An error occurred with the debugger connection!", findAssetId("Small"));
|
||||
});
|
||||
patchConsoleAndLogger();
|
||||
});
|
||||
|
||||
socket.addEventListener("message", (message: any) => {
|
||||
try {
|
||||
const data = JSON.parse(message.data);
|
||||
|
||||
if (data.type === MessageType.Run && data.data?.code) {
|
||||
try {
|
||||
(0, eval)(data.data.code);
|
||||
} catch (e) {
|
||||
console.error("Error executing remote code:", e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
(0, eval)(message.data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener("close", () => {
|
||||
showToast("Disconnected from debugger.", findAssetId("Small"));
|
||||
unpatchConsoleAndLogger();
|
||||
});
|
||||
|
||||
socket.addEventListener("error", (err: any) => {
|
||||
console.log(`Debugger error: ${err.message}`);
|
||||
showToast("An error occurred with the debugger connection!", findAssetId("Small"));
|
||||
unpatchConsoleAndLogger();
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Failed to connect to debugger:", e);
|
||||
showToast("Failed to connect to debugger!", findAssetId("Small"));
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnectFromDebugger() {
|
||||
if (socket) {
|
||||
unpatchConsoleAndLogger();
|
||||
socket.close();
|
||||
socket = undefined;
|
||||
showToast("Disconnected from debugger.", findAssetId("Check"));
|
||||
}
|
||||
}
|
||||
|
||||
export function isConnectedToDebugger(): boolean {
|
||||
return socket?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
// --- React DevTools (RDT) Logic ---
|
||||
const rdtPort = 8097;
|
||||
export let rdtClient: WebSocket | null = null;
|
||||
export let rdtConnected = false;
|
||||
const changeHooks = new Set<(value: boolean) => void>();
|
||||
|
||||
function bump() {
|
||||
for (const x of changeHooks) x(rdtConnected);
|
||||
}
|
||||
|
||||
function cleanupRdt() {
|
||||
rdtClient = null;
|
||||
rdtConnected = false;
|
||||
bump();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function connectRdt(url: string, quiet?: boolean) {
|
||||
if (!isReactDevToolsPreloaded() || rdtClient) return;
|
||||
|
||||
const base = url.split(":").slice(0, -1).join(":");
|
||||
const ws = (rdtClient = new WebSocket(`ws://${base}:${rdtPort}`));
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
if (!quiet) showToast("Connected to React DevTools", findAssetId("CheckmarkSmallIcon"));
|
||||
rdtConnected = true;
|
||||
bump();
|
||||
});
|
||||
|
||||
ws.addEventListener('close', () => {
|
||||
cleanupRdt();
|
||||
});
|
||||
|
||||
ws.addEventListener('error', (e: any) => {
|
||||
cleanupRdt();
|
||||
const err = e?.message ?? e?.stack ?? String(e);
|
||||
logger.error('React DevTools error:', err);
|
||||
if (!quiet) showToast(err, findAssetId("CircleXIcon-primary"));
|
||||
});
|
||||
|
||||
const devTools = window[getReactDevToolsProp() || "__vendetta_rdc"];
|
||||
if (devTools?.connectToDevTools) {
|
||||
devTools.connectToDevTools({
|
||||
websocket: ws,
|
||||
resolveRNStyle: StyleSheet.flatten,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnectRdt() {
|
||||
rdtClient?.close();
|
||||
}
|
||||
|
||||
export function useIsRdtConnected() {
|
||||
const [connected, update] = React.useState(rdtConnected);
|
||||
|
||||
React.useEffect(() => {
|
||||
changeHooks.add(update);
|
||||
return () => void changeHooks.delete(update);
|
||||
}, []);
|
||||
|
||||
return connected;
|
||||
}
|
||||
|
||||
// --- Core Utils ---
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function patchLogHook() {
|
||||
const unpatch = after("nativeLoggingHook", globalThis, args => {
|
||||
if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ message: args[0], level: args[1] }));
|
||||
if (socket?.readyState === WebSocket.OPEN) {
|
||||
sendLog(args[1] === 'error' ? LogLevel.Error : args[1] === 'warn' ? LogLevel.Warn : LogLevel.Default, args[0]);
|
||||
}
|
||||
logger.log(args[0]);
|
||||
});
|
||||
|
||||
|
|
@ -89,19 +341,13 @@ export function patchLogHook() {
|
|||
export const versionHash = version;
|
||||
|
||||
export function getDebugInfo() {
|
||||
// Hermes
|
||||
const hermesProps = window.HermesInternal.getRuntimeProperties();
|
||||
const hermesVer = hermesProps["OSS Release Version"];
|
||||
const padding = "for RN ";
|
||||
|
||||
// RN
|
||||
const PlatformConstants = Platform.constants as RNConstants;
|
||||
const rnVer = PlatformConstants.reactNativeVersion;
|
||||
|
||||
return {
|
||||
/**
|
||||
* @deprecated use `bunny` field
|
||||
* */
|
||||
vendetta: {
|
||||
version: versionHash.split("-")[0],
|
||||
loader: getLoaderName(),
|
||||
|
|
@ -126,76 +372,35 @@ export function getDebugInfo() {
|
|||
buildType: hermesProps.Build,
|
||||
bytecodeVersion: hermesProps["Bytecode Version"],
|
||||
},
|
||||
...Platform.select(
|
||||
{
|
||||
android: {
|
||||
os: {
|
||||
name: "Android",
|
||||
version: PlatformConstants.Release,
|
||||
sdk: PlatformConstants.Version
|
||||
},
|
||||
},
|
||||
ios: {
|
||||
os: {
|
||||
name: PlatformConstants.systemName,
|
||||
version: PlatformConstants.osVersion
|
||||
},
|
||||
}
|
||||
}
|
||||
)!,
|
||||
...Platform.select(
|
||||
{
|
||||
android: {
|
||||
device: {
|
||||
manufacturer: PlatformConstants.Manufacturer,
|
||||
brand: PlatformConstants.Brand,
|
||||
model: PlatformConstants.Model,
|
||||
codename: NativeDeviceModule.device
|
||||
}
|
||||
},
|
||||
ios: {
|
||||
device: {
|
||||
manufacturer: NativeDeviceModule.deviceManufacturer,
|
||||
brand: NativeDeviceModule.deviceBrand,
|
||||
model: NativeDeviceModule.deviceModel,
|
||||
codename: NativeDeviceModule.device
|
||||
}
|
||||
}
|
||||
}
|
||||
)!
|
||||
...Platform.select({
|
||||
android: { os: { name: "Android", version: PlatformConstants.Release, sdk: PlatformConstants.Version } },
|
||||
ios: { os: { name: PlatformConstants.systemName, version: PlatformConstants.osVersion } }
|
||||
})!,
|
||||
...Platform.select({
|
||||
android: { device: { manufacturer: PlatformConstants.Manufacturer, brand: PlatformConstants.Brand, model: PlatformConstants.Model, codename: NativeDeviceModule.device } },
|
||||
ios: { device: { manufacturer: NativeDeviceModule.deviceManufacturer, brand: NativeDeviceModule.deviceBrand, model: NativeDeviceModule.deviceModel, codename: NativeDeviceModule.device } }
|
||||
})!
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function initDebugger() {
|
||||
if (settings.autoDebugger) {
|
||||
try {
|
||||
connectToDebugger(settings.debuggerUrl);
|
||||
} catch (e) {
|
||||
logger.error("Failed to connect to Debugger during startup:", e);
|
||||
}
|
||||
try {
|
||||
connectToDebugger(settings.debuggerUrl);
|
||||
} catch (e) {
|
||||
logger.error("Failed to connect to Debugger during startup:", e);
|
||||
}
|
||||
}
|
||||
if (settings.autoDevTools) {
|
||||
try {
|
||||
(async () => {
|
||||
const devTools = window[getReactDevToolsProp() || "__vendetta_rdc"];
|
||||
|
||||
if (!devTools?.connectToDevTools) {
|
||||
showToast("Invalid devTools URL!", findAssetId("Small"));
|
||||
return;
|
||||
}
|
||||
|
||||
await devTools.connectToDevTools({
|
||||
host: settings.devToolsUrl.split(":")?.[0],
|
||||
resolveRNStyle: StyleSheet.flatten,
|
||||
});
|
||||
})();
|
||||
} catch (e) {
|
||||
logger.error("Failed to connect to ReactDevTools during startup:", e);
|
||||
}
|
||||
try {
|
||||
if (settings.devToolsUrl) {
|
||||
connectRdt(settings.devToolsUrl, true);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Failed to connect to ReactDevTools during startup:", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -175,10 +175,7 @@ export function getThemeFilePath() {
|
|||
|
||||
export function isReactDevToolsPreloaded() {
|
||||
if (isPyonLoader()) {
|
||||
return Boolean(window.__reactDevTools);
|
||||
}
|
||||
if (isRa1nLoader()) {
|
||||
return false;
|
||||
return Boolean(window.__REACT_DEVTOOLS__);
|
||||
}
|
||||
if (isVendettaLoader()) {
|
||||
return vendettaLoaderIdentity!!.features.devtools != null;
|
||||
|
|
@ -191,7 +188,7 @@ export function getReactDevToolsProp(): string | null {
|
|||
if (!isReactDevToolsPreloaded()) return null;
|
||||
|
||||
if (isPyonLoader()) {
|
||||
window.__pyoncord_rdt = window.__reactDevTools.exports;
|
||||
window.__pyoncord_rdt = window.__REACT_DEVTOOLS__.exports;
|
||||
return "__pyoncord_rdt";
|
||||
}
|
||||
|
||||
|
|
@ -206,7 +203,7 @@ export function getReactDevToolsVersion() {
|
|||
if (!isReactDevToolsPreloaded()) return null;
|
||||
|
||||
if (isPyonLoader()) {
|
||||
return window.__reactDevTools.version || null;
|
||||
return window.__REACT_DEVTOOLS__.version || null;
|
||||
}
|
||||
if (isVendettaLoader()) {
|
||||
return vendettaLoaderIdentity!!.features.devtools!!.version;
|
||||
|
|
|
|||
67
src/lib/api/reactdevtools.ts
Normal file
67
src/lib/api/reactdevtools.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { logger } from "@lib/utils/logger";
|
||||
import { getReactDevToolsProp, isReactDevToolsPreloaded } from "./native/loader";
|
||||
import { showToast } from "@lib/ui/toasts";
|
||||
import { findAssetId } from "./assets";
|
||||
|
||||
const rdtPort = 8097;
|
||||
|
||||
export let rdtClient: WebSocket | null = null;
|
||||
export let rdtConnected = false;
|
||||
|
||||
const changeHooks = new Set<(value: boolean) => void>();
|
||||
|
||||
function bump() {
|
||||
for (const x of changeHooks) x(rdtConnected);
|
||||
}
|
||||
function cleanup() {
|
||||
rdtClient = null;
|
||||
rdtConnected = false;
|
||||
bump();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function connectRdt(url: string, quiet?: boolean) {
|
||||
if (!isReactDevToolsPreloaded() || rdtClient) return;
|
||||
|
||||
const base = url.split(":").slice(0, -1);
|
||||
const ws = (rdtClient = new WebSocket(`ws://${base}:${rdtPort}`));
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
if (!quiet) showToast("Connected to React DevTools", findAssetId("CheckmarkSmallIcon"));
|
||||
|
||||
rdtConnected = true;
|
||||
bump();
|
||||
});
|
||||
|
||||
ws.addEventListener('close', () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
ws.addEventListener('error', (e: any) => {
|
||||
cleanup();
|
||||
|
||||
const err = e?.message ?? e?.stack ?? String(e);
|
||||
logger.error('React DevTools error:', err);
|
||||
|
||||
if (!quiet) showToast(err, findAssetId("CircleXIcon-primary"));
|
||||
});
|
||||
|
||||
window[getReactDevToolsProp() || "__vendetta_rdc"].connectToDevTools({
|
||||
websocket: ws,
|
||||
});
|
||||
}
|
||||
|
||||
export function disconnectRdt() {
|
||||
rdtClient?.close();
|
||||
}
|
||||
|
||||
export function useIsRdtConnected() {
|
||||
const [connected, update] = React.useState(rdtConnected);
|
||||
|
||||
React.useEffect(() => {
|
||||
changeHooks.add(update);
|
||||
return () => void changeHooks.delete(update);
|
||||
});
|
||||
|
||||
return connected;
|
||||
}
|
||||
|
|
@ -1,8 +1,18 @@
|
|||
import { Emitter } from "@core/vendetta/Emitter";
|
||||
import { Observable, ObserverOptions } from "@gullerya/object-observer";
|
||||
import type { Observable as ObservableType, ObserverOptions } from "@gullerya/object-observer";
|
||||
import { fileExists, readFile, removeFile, writeFile } from "@lib/api/native/fs";
|
||||
import { debounce } from "es-toolkit";
|
||||
|
||||
let _ObservableRuntime: any = null;
|
||||
function getObservableRuntime() {
|
||||
if (_ObservableRuntime) return _ObservableRuntime;
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const mod = require("@gullerya/object-observer");
|
||||
// the module may export named members or the Observable directly
|
||||
_ObservableRuntime = mod.Observable ?? mod;
|
||||
return _ObservableRuntime;
|
||||
}
|
||||
|
||||
const storageInitErrorSymbol = Symbol.for("bunny.storage.initError");
|
||||
const storagePromiseSymbol = Symbol.for("bunny.storage.promise");
|
||||
|
||||
|
|
@ -34,12 +44,12 @@ function createFileBackend<T extends object>(filePath: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export function useObservable(observables: Observable[], opts?: ObserverOptions) {
|
||||
export function useObservable(observables: ObservableType[], opts?: ObserverOptions) {
|
||||
if (observables.some((o: any) => o?.[storageInitErrorSymbol])) throw new Error(
|
||||
"An error occured while initializing the storage",
|
||||
);
|
||||
|
||||
if (observables.some(o => !Observable.isObservable(o))) {
|
||||
if (observables.some(o => !getObservableRuntime().isObservable(o))) {
|
||||
throw new Error("Argument passed isn't an Observable");
|
||||
}
|
||||
|
||||
|
|
@ -48,10 +58,10 @@ export function useObservable(observables: Observable[], opts?: ObserverOptions)
|
|||
React.useEffect(() => {
|
||||
const listener = () => forceUpdate();
|
||||
|
||||
observables.forEach(o => Observable.observe(o, listener, opts));
|
||||
observables.forEach(o => getObservableRuntime().observe(o, listener, opts));
|
||||
|
||||
return () => {
|
||||
observables.forEach(o => Observable.unobserve(o, listener));
|
||||
observables.forEach(o => getObservableRuntime().unobserve(o, listener));
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
|
@ -72,13 +82,13 @@ export function createStorageAndCallback<T extends object = {}>(
|
|||
let emitter: Emitter;
|
||||
|
||||
const callback = (data: any) => {
|
||||
const proxy = new Proxy(Observable.from(data), {
|
||||
const proxy = new Proxy(getObservableRuntime().from(data), {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === Symbol.for("vendetta.storage.emitter")) {
|
||||
if (emitter) return emitter;
|
||||
emitter = new Emitter();
|
||||
|
||||
Observable.observe(target, changes => {
|
||||
getObservableRuntime().observe(target, changes => {
|
||||
for (const change of changes) {
|
||||
emitter.emit(change.type !== "delete" ? "SET" : "DEL", {
|
||||
path: change.path,
|
||||
|
|
@ -95,7 +105,7 @@ export function createStorageAndCallback<T extends object = {}>(
|
|||
});
|
||||
|
||||
const handler = () => backend.set(proxy);
|
||||
Observable.observe(proxy, handler);
|
||||
getObservableRuntime().observe(proxy, handler);
|
||||
|
||||
cb(proxy);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,34 +4,56 @@ import { createStyles } from "@ui/styles";
|
|||
import { Platform, Text, TextInput } from "react-native";
|
||||
|
||||
export interface CodeblockProps {
|
||||
selectable?: boolean;
|
||||
style?: import("react-native").TextStyle;
|
||||
children?: string;
|
||||
selectable?: boolean;
|
||||
style?: import("react-native").TextStyle;
|
||||
children?: string;
|
||||
}
|
||||
|
||||
const useStyles = createStyles({
|
||||
codeBlock: {
|
||||
fontFamily: constants.Fonts.CODE_NORMAL,
|
||||
fontSize: 12,
|
||||
textAlignVertical: "center",
|
||||
backgroundColor: semanticColors.BACKGROUND_SECONDARY,
|
||||
color: semanticColors.TEXT_NORMAL,
|
||||
borderWidth: 1,
|
||||
borderRadius: 12,
|
||||
borderColor: semanticColors.BACKGROUND_TERTIARY,
|
||||
padding: 10,
|
||||
},
|
||||
codeBlock: {
|
||||
fontFamily: constants.Fonts.CODE_NORMAL,
|
||||
fontSize: 12,
|
||||
textAlignVertical: "center",
|
||||
backgroundColor: semanticColors.CARD_BACKGROUND_DEFAULT,
|
||||
color: semanticColors.TEXT_DEFAULT,
|
||||
borderWidth: 1,
|
||||
borderRadius: 12,
|
||||
borderColor: semanticColors.BORDER_SUBTLE,
|
||||
padding: 10,
|
||||
},
|
||||
});
|
||||
|
||||
// iOS doesn't support the selectable property on RN.Text...
|
||||
const InputBasedCodeblock = ({ style, children }: CodeblockProps) => <TextInput editable={false} multiline style={[useStyles().codeBlock, style && style]} value={children} />;
|
||||
const TextBasedCodeblock = ({ selectable, style, children }: CodeblockProps) => <Text selectable={selectable} style={[useStyles().codeBlock, style && style]}>{children}</Text>;
|
||||
const InputBasedCodeblock = ({ style, children }: CodeblockProps) => (
|
||||
<TextInput
|
||||
editable={false}
|
||||
multiline
|
||||
style={[useStyles().codeBlock, style && style]}
|
||||
value={children}
|
||||
/>
|
||||
);
|
||||
const TextBasedCodeblock = ({
|
||||
selectable,
|
||||
style,
|
||||
children,
|
||||
}: CodeblockProps) => (
|
||||
<Text selectable={selectable} style={[useStyles().codeBlock, style && style]}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
export default function Codeblock({ selectable, style, children }: CodeblockProps) {
|
||||
if (!selectable) return <TextBasedCodeblock style={style} children={children} />;
|
||||
export default function Codeblock({
|
||||
selectable,
|
||||
style,
|
||||
children,
|
||||
}: CodeblockProps) {
|
||||
if (!selectable)
|
||||
return <TextBasedCodeblock style={style} children={children} />;
|
||||
|
||||
return Platform.select({
|
||||
ios: <InputBasedCodeblock style={style} children={children} />,
|
||||
default: <TextBasedCodeblock style={style} children={children} selectable />,
|
||||
});
|
||||
return Platform.select({
|
||||
ios: <InputBasedCodeblock style={style} children={children} />,
|
||||
default: (
|
||||
<TextBasedCodeblock style={style} children={children} selectable />
|
||||
),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { findByNameLazy } from "@metro/wrappers";
|
||||
|
||||
type LoggerFunction = (...messages: any[]) => void;
|
||||
|
||||
export interface Logger {
|
||||
log: LoggerFunction;
|
||||
info: LoggerFunction;
|
||||
|
|
@ -13,3 +14,185 @@ export interface Logger {
|
|||
|
||||
export const LoggerClass = findByNameLazy("Logger");
|
||||
export const logger: Logger = new LoggerClass("ShiggyCord");
|
||||
|
||||
let socket: WebSocket | undefined;
|
||||
let originalConsoleLog: any;
|
||||
let originalConsoleError: any;
|
||||
let originalConsoleWarn: any;
|
||||
let originalLoggerLog: any;
|
||||
let originalLoggerError: any;
|
||||
let originalLoggerWarn: any;
|
||||
|
||||
interface LogMessage {
|
||||
type: 'log';
|
||||
data: {
|
||||
level: 'debug' | 'default' | 'warn' | 'error';
|
||||
message: any[];
|
||||
};
|
||||
}
|
||||
|
||||
interface HelloMessage {
|
||||
type: 'hello';
|
||||
data: {
|
||||
version: number;
|
||||
};
|
||||
}
|
||||
|
||||
const VERSION = 1;
|
||||
|
||||
function serializeMessage(msg: any): string {
|
||||
return JSON.stringify(msg);
|
||||
}
|
||||
|
||||
function sendLog(level: 'debug' | 'default' | 'warn' | 'error', ...args: any[]) {
|
||||
if (socket?.readyState === WebSocket.OPEN) {
|
||||
const message: LogMessage = {
|
||||
type: 'log',
|
||||
data: {
|
||||
level,
|
||||
message: args
|
||||
}
|
||||
};
|
||||
socket.send(serializeMessage(message));
|
||||
}
|
||||
}
|
||||
|
||||
function patchConsoleAndLogger() {
|
||||
originalConsoleLog = console.log;
|
||||
console.log = function(...args: any[]) {
|
||||
originalConsoleLog.apply(console, args);
|
||||
sendLog('default', ...args);
|
||||
};
|
||||
|
||||
originalConsoleError = console.error;
|
||||
console.error = function(...args: any[]) {
|
||||
originalConsoleError.apply(console, args);
|
||||
sendLog('error', ...args);
|
||||
};
|
||||
|
||||
originalConsoleWarn = console.warn;
|
||||
console.warn = function(...args: any[]) {
|
||||
originalConsoleWarn.apply(console, args);
|
||||
sendLog('warn', ...args);
|
||||
};
|
||||
|
||||
if (logger) {
|
||||
originalLoggerLog = logger.log;
|
||||
logger.log = function(...args: any[]) {
|
||||
originalLoggerLog.apply(logger, args);
|
||||
sendLog('default', ...args);
|
||||
};
|
||||
|
||||
originalLoggerError = logger.error;
|
||||
logger.error = function(...args: any[]) {
|
||||
originalLoggerError.apply(logger, args);
|
||||
sendLog('error', ...args);
|
||||
};
|
||||
|
||||
originalLoggerWarn = logger.warn;
|
||||
logger.warn = function(...args: any[]) {
|
||||
originalLoggerWarn.apply(logger, args);
|
||||
sendLog('warn', ...args);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function unpatchConsoleAndLogger() {
|
||||
if (originalConsoleLog) {
|
||||
console.log = originalConsoleLog;
|
||||
originalConsoleLog = undefined;
|
||||
}
|
||||
if (originalConsoleError) {
|
||||
console.error = originalConsoleError;
|
||||
originalConsoleError = undefined;
|
||||
}
|
||||
if (originalConsoleWarn) {
|
||||
console.warn = originalConsoleWarn;
|
||||
originalConsoleWarn = undefined;
|
||||
}
|
||||
|
||||
if (logger) {
|
||||
if (originalLoggerLog) {
|
||||
logger.log = originalLoggerLog;
|
||||
originalLoggerLog = undefined;
|
||||
}
|
||||
if (originalLoggerError) {
|
||||
logger.error = originalLoggerError;
|
||||
originalLoggerError = undefined;
|
||||
}
|
||||
if (originalLoggerWarn) {
|
||||
logger.warn = originalLoggerWarn;
|
||||
originalLoggerWarn = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function connectToDebugger(url: string) {
|
||||
if (socket !== undefined && socket.readyState !== WebSocket.CLOSED) {
|
||||
unpatchConsoleAndLogger();
|
||||
socket.close();
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
console.error("Invalid debugger URL!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
socket = new WebSocket(`ws://${url}`);
|
||||
|
||||
socket.addEventListener("open", () => {
|
||||
console.log("Connected to debugger.");
|
||||
|
||||
const hello: HelloMessage = {
|
||||
type: 'hello',
|
||||
data: {
|
||||
version: VERSION
|
||||
}
|
||||
};
|
||||
socket?.send(serializeMessage(hello));
|
||||
|
||||
patchConsoleAndLogger();
|
||||
});
|
||||
|
||||
socket.addEventListener("message", (message: any) => {
|
||||
try {
|
||||
const data = JSON.parse(message.data);
|
||||
|
||||
if (data.type === 'run' && data.data?.code) {
|
||||
try {
|
||||
(0, eval)(data.data.code);
|
||||
} catch (e) {
|
||||
console.error("Error executing remote code:", e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error processing message:", e);
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener("close", () => {
|
||||
console.log("Disconnected from debugger.");
|
||||
unpatchConsoleAndLogger();
|
||||
});
|
||||
|
||||
socket.addEventListener("error", (err: any) => {
|
||||
console.error(`Debugger error: ${err.message}`);
|
||||
unpatchConsoleAndLogger();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to connect to debugger:", e);
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnectFromDebugger() {
|
||||
if (socket) {
|
||||
unpatchConsoleAndLogger();
|
||||
socket.close();
|
||||
socket = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function isConnectedToDebugger(): boolean {
|
||||
return socket?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
|
|
|||
268
src/lib/utils/timings.ts
Normal file
268
src/lib/utils/timings.ts
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
/**
|
||||
* Lightweight timing utility for boot / init instrumentation.
|
||||
*
|
||||
* Usage examples:
|
||||
* import { timings } from "@lib/utils/timings";
|
||||
*
|
||||
* // measure synchronous function
|
||||
* const res = timings.measureSync("initThemes", () => initThemes());
|
||||
*
|
||||
* // measure async function
|
||||
* await timings.measureAsync("loadPlugins", async () => { await loadPlugins(); });
|
||||
*
|
||||
* // manual start/stop
|
||||
* const stop = timings.start("heavyTask");
|
||||
* doHeavyWork();
|
||||
* stop();
|
||||
*
|
||||
* // inspect / log results
|
||||
* timings.logReport(); // prints a concise table to console
|
||||
*
|
||||
* Goals:
|
||||
* - Minimal, zero-dep helper to identify slow initializers.
|
||||
* - Safe for environments without `performance` (falls back to Date.now).
|
||||
* - Small memory footprint and simple API.
|
||||
*/
|
||||
|
||||
type Stat = {
|
||||
name: string;
|
||||
calls: number;
|
||||
totalMs: number;
|
||||
lastMs: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
};
|
||||
|
||||
type ReportRow = {
|
||||
name: string;
|
||||
calls: number;
|
||||
totalMs: number;
|
||||
avgMs: number;
|
||||
lastMs: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
};
|
||||
|
||||
const now = (() => {
|
||||
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
||||
return () => performance.now();
|
||||
}
|
||||
return () => Date.now();
|
||||
})();
|
||||
|
||||
/**
|
||||
* Internal stats map: key -> Stat
|
||||
*/
|
||||
const _stats = new Map<string, Stat>();
|
||||
|
||||
/**
|
||||
* Start a manual timer for `name`. Returns a stop() function that records the
|
||||
* elapsed time into the statistics and returns the duration.
|
||||
*
|
||||
* Example:
|
||||
* const stop = start('db.init');
|
||||
* // ... work ...
|
||||
* const ms = stop();
|
||||
*/
|
||||
export function start(name: string) {
|
||||
const t0 = now();
|
||||
let stopped = false;
|
||||
|
||||
return function stop() {
|
||||
if (stopped) return 0;
|
||||
stopped = true;
|
||||
const duration = Math.max(0, now() - t0);
|
||||
record(name, duration);
|
||||
return duration;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure a synchronous function and record its duration.
|
||||
* Returns the function result.
|
||||
*/
|
||||
export function measureSync<T>(name: string, fn: () => T): T {
|
||||
const t0 = now();
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
const duration = Math.max(0, now() - t0);
|
||||
record(name, duration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure an async function (promise-returning) and record its duration.
|
||||
* Returns the awaited result.
|
||||
*/
|
||||
export async function measureAsync<T>(name: string, fn: () => Promise<T>): Promise<T> {
|
||||
const t0 = now();
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
const duration = Math.max(0, now() - t0);
|
||||
record(name, duration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a function (sync or async) preserving its return value and recording the timing.
|
||||
* If fn returns a Promise it will await it and record the time.
|
||||
*/
|
||||
export function wrap<T extends (...a: any[]) => any>(name: string, fn: T): T {
|
||||
const wrapped = function (this: any, ...args: any[]) {
|
||||
const t0 = now();
|
||||
try {
|
||||
const res = (fn as any).apply(this, args);
|
||||
if (res && typeof res.then === "function") {
|
||||
// async
|
||||
return res.finally(() => {
|
||||
const duration = Math.max(0, now() - t0);
|
||||
record(name, duration);
|
||||
});
|
||||
}
|
||||
// sync
|
||||
return res;
|
||||
} finally {
|
||||
// for synchronous returns, ensure timing recorded in finally above won't double-record.
|
||||
if ((fn as any).constructor?.name !== "AsyncFunction") {
|
||||
const duration = Math.max(0, now() - t0);
|
||||
record(name, duration);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (wrapped as unknown) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a measured duration (milliseconds) under `name`.
|
||||
*/
|
||||
export function record(name: string, durationMs: number) {
|
||||
if (!name) name = "<unnamed>";
|
||||
const s = _stats.get(name);
|
||||
if (!s) {
|
||||
_stats.set(name, {
|
||||
name,
|
||||
calls: 1,
|
||||
totalMs: durationMs,
|
||||
lastMs: durationMs,
|
||||
minMs: durationMs,
|
||||
maxMs: durationMs,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
s.calls += 1;
|
||||
s.lastMs = durationMs;
|
||||
s.totalMs += durationMs;
|
||||
if (durationMs < s.minMs) s.minMs = durationMs;
|
||||
if (durationMs > s.maxMs) s.maxMs = durationMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a sorted report array (desc by total time) limited to `limit` rows.
|
||||
*/
|
||||
export function report(limit = 25): ReportRow[] {
|
||||
const rows: ReportRow[] = [];
|
||||
for (const [, s] of _stats) {
|
||||
rows.push({
|
||||
name: s.name,
|
||||
calls: s.calls,
|
||||
totalMs: s.totalMs,
|
||||
avgMs: s.calls > 0 ? s.totalMs / s.calls : 0,
|
||||
lastMs: s.lastMs,
|
||||
minMs: s.minMs,
|
||||
maxMs: s.maxMs,
|
||||
});
|
||||
}
|
||||
|
||||
rows.sort((a, b) => b.totalMs - a.totalMs);
|
||||
return rows.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Console-log a human-readable report.
|
||||
*/
|
||||
export function logReport(limit = 25) {
|
||||
const rows = report(limit);
|
||||
if (rows.length === 0) {
|
||||
console.info("[timings] no recorded timings");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a compact table-like output
|
||||
console.groupCollapsed(`[timings] top ${rows.length} (by total ms)`);
|
||||
try {
|
||||
console.table(
|
||||
rows.map((r) => ({
|
||||
name: r.name,
|
||||
calls: r.calls,
|
||||
totalMs: Number(r.totalMs.toFixed(2)),
|
||||
avgMs: Number(r.avgMs.toFixed(2)),
|
||||
lastMs: Number(r.lastMs.toFixed(2)),
|
||||
minMs: Number(r.minMs.toFixed(2)),
|
||||
maxMs: Number(r.maxMs.toFixed(2)),
|
||||
})),
|
||||
);
|
||||
} catch {
|
||||
// Some environments don't support console.table
|
||||
for (const r of rows) {
|
||||
console.log(
|
||||
`${r.name} — total ${r.totalMs.toFixed(2)}ms • avg ${r.avgMs.toFixed(2)}ms • last ${r.lastMs.toFixed(
|
||||
2,
|
||||
)}ms • calls ${r.calls}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear collected timing stats.
|
||||
*/
|
||||
export function clear() {
|
||||
_stats.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: record but return value for fluent usage.
|
||||
* Example:
|
||||
* const data = await timings.withTimingAsync('fetch', () => fetchJSON());
|
||||
*/
|
||||
export async function withTimingAsync<T>(name: string, fn: () => Promise<T>): Promise<T> {
|
||||
const t0 = now();
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
record(name, Math.max(0, now() - t0));
|
||||
}
|
||||
}
|
||||
|
||||
export function withTimingSync<T>(name: string, fn: () => T): T {
|
||||
const t0 = now();
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
record(name, Math.max(0, now() - t0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose a small default export for convenience.
|
||||
*/
|
||||
export const timings = {
|
||||
start,
|
||||
measureSync,
|
||||
measureAsync,
|
||||
wrap,
|
||||
record,
|
||||
report,
|
||||
logReport,
|
||||
clear,
|
||||
withTimingAsync,
|
||||
withTimingSync,
|
||||
};
|
||||
|
||||
export default timings;
|
||||
Loading…
Add table
Reference in a new issue