fix many bugs found by Claude (#4436)
Fixes: - Webpack Patcher: incorrect patch error handling could lead to some functional patches not being applied - CommandsAPI: subcommands were impossible to unregister - useAwaiter: on error, the value was set to null even if a fallbackValue was specified, violating the type contract - Patcher: build check boolean logic was grouped incorrectly due to missing parantheses - CustomCommands: commands weren't unregistered on stop, leading to error when enabling, disabling & re-renabling the plugin - Notification Log: delete was by timestamp instead of ID and non-atomic - FixSpotifyEmbeds: settings listener was registered inside browser-window-created event handler, leading to duplicate listeners - CustomRPC: changing activity type or timestamp didn't update the RPC - CopyStickerLinks: message patch had non-sense code, just removed it - XSOverlay: UDP socket wasn't closed on stop - VoiceMessages: Web recorder never closed the mic media stream, which continues showing the OS mic indicator - PictureInPicture: Video clone wasn't properly cleaned up, which leaked memory - Themes (Extension version): theme object urls were never cleaned up, which leaked memory on theme change - Themes (Extension version): Upload Theme button hitbox was only the text, not the whole button. Also added Drag & Drop from file manager support - Dearrow: toggling for one embed flipped the global dearrow by default setting - Extension Installer: the zip extract function was vulnerable to path traversal, although we only ever used this for React DevTools obtained via https so it has low impact - FileWatchers: ensure always properly cleaned up - components/Link: mutated props which is a very bad practice All fixes are human written, only the audit was done by Claude Fable (thanks for ur tokens, Zere).
This commit is contained in:
parent
73d8490b01
commit
2f1c6a37fc
29 changed files with 310 additions and 143 deletions
|
|
@ -40,7 +40,7 @@ export interface ProfileBadge {
|
|||
/** Action to perform when you click the badge */
|
||||
onClick?(event: React.MouseEvent, props: ProfileBadge & BadgeUserArgs): void;
|
||||
/** Action to perform when you right click the badge */
|
||||
onContextMenu?(event: React.MouseEvent, props: BadgeUserArgs & BadgeUserArgs): void;
|
||||
onContextMenu?(event: React.MouseEvent, props: ProfileBadge & BadgeUserArgs): void;
|
||||
/** Should the user display this badge? */
|
||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
||||
|
|
|
|||
|
|
@ -46,16 +46,14 @@ export let OptionalMessageOption: CommandOption = OptPlaceholder;
|
|||
*/
|
||||
export let RequiredMessageOption: CommandOption = ReqPlaceholder;
|
||||
|
||||
// Discord's command list has random gaps for some reason, which can cause issues while rendering the commands
|
||||
// Add this offset to every added command to keep them unique
|
||||
let commandIdOffset: number;
|
||||
let idCounter = 99;
|
||||
|
||||
export const _init = function (cmds: VencordCommand[]) {
|
||||
try {
|
||||
BUILT_IN = cmds;
|
||||
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
|
||||
RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0];
|
||||
commandIdOffset = Math.abs(BUILT_IN.map(x => Number(x.id)).sort((x, y) => x - y)[0]) - BUILT_IN.length;
|
||||
idCounter = Math.abs(BUILT_IN.map(x => Number(x.id)).sort((x, y) => x - y)[0]) + 1;
|
||||
} catch (e) {
|
||||
new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds);
|
||||
}
|
||||
|
|
@ -107,21 +105,23 @@ export function prepareOption<O extends CommandOption | VencordCommand>(opt: O):
|
|||
return opt;
|
||||
}
|
||||
|
||||
const isSubCommandParent = (cmd: VencordCommand) => cmd.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND;
|
||||
const getSubCommandName = (cmd: VencordCommand, option: CommandOption) => `${cmd.name} ${option.name}`;
|
||||
|
||||
// Yes, Discord registers individual commands for each subcommand
|
||||
// TODO: This probably doesn't support nested subcommands. If that is ever needed,
|
||||
// investigate
|
||||
function registerSubCommands(cmd: VencordCommand, plugin: string) {
|
||||
cmd.options?.forEach(o => {
|
||||
if (o.type !== ApplicationCommandOptionType.SUB_COMMAND)
|
||||
throw new Error("When specifying sub-command options, all options must be sub-commands.");
|
||||
|
||||
const subCmd = {
|
||||
...cmd,
|
||||
...o,
|
||||
options: o.options !== undefined ? o.options : undefined,
|
||||
type: ApplicationCommandType.CHAT_INPUT,
|
||||
name: `${cmd.name} ${o.name}`,
|
||||
id: `${o.name}-${cmd.id}`,
|
||||
displayName: `${cmd.name} ${o.name}`,
|
||||
name: getSubCommandName(cmd, o),
|
||||
displayName: getSubCommandName(cmd, o),
|
||||
subCommandPath: [{
|
||||
name: o.name,
|
||||
type: o.type,
|
||||
|
|
@ -129,10 +129,18 @@ function registerSubCommands(cmd: VencordCommand, plugin: string) {
|
|||
}],
|
||||
rootCommand: cmd
|
||||
};
|
||||
registerCommand(subCmd as any, plugin);
|
||||
registerCommand(subCmd, plugin);
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterSubCommands(cmd: VencordCommand): boolean {
|
||||
const results = BUILT_IN
|
||||
.filter(c => c.rootCommand === cmd)
|
||||
.map(c => unregisterCommand(c.name));
|
||||
|
||||
return results.length > 0 && results.every(x => x);
|
||||
}
|
||||
|
||||
export function registerCommand<C extends VencordCommand>(command: C, plugin: string) {
|
||||
if (!BUILT_IN) {
|
||||
console.warn(
|
||||
|
|
@ -149,24 +157,30 @@ export function registerCommand<C extends VencordCommand>(command: C, plugin: st
|
|||
command.isVencordCommand = true;
|
||||
command.untranslatedName ??= command.name;
|
||||
command.untranslatedDescription ??= command.description;
|
||||
command.id ??= `-${BUILT_IN.length + commandIdOffset + 1}`;
|
||||
command.id ??= `-${idCounter++}`;
|
||||
command.applicationId ??= "-1"; // BUILT_IN;
|
||||
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
||||
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
|
||||
command.plugin ||= plugin;
|
||||
|
||||
prepareOption(command);
|
||||
commands[command.name] = command;
|
||||
|
||||
if (command.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND) {
|
||||
if (isSubCommandParent(command)) {
|
||||
registerSubCommands(command, plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
commands[command.name] = command;
|
||||
BUILT_IN.push(command);
|
||||
}
|
||||
|
||||
export function unregisterCommand(name: string) {
|
||||
export function unregisterCommand(name: string, isSubCommands = false) {
|
||||
const cmd = commands[name];
|
||||
if (cmd && isSubCommandParent(cmd)) {
|
||||
delete commands[name];
|
||||
return unregisterSubCommands(cmd);
|
||||
}
|
||||
|
||||
const idx = BUILT_IN.findIndex(c => c.name === name);
|
||||
if (idx === -1)
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@ export { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationC
|
|||
|
||||
export interface VencordCommand extends Command {
|
||||
isVencordCommand?: boolean;
|
||||
rootCommand?: VencordCommand;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { openNotificationSettingsModal } from "@components/settings/tabs/vencord
|
|||
import { classNameFactory } from "@utils/css";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { ConfirmModal, Forms, ListScrollerThin, Modal,openModal, React, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
||||
import { ConfirmModal, Forms, ListScrollerThin, Modal, openModal, React, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { DispatchWithoutAction } from "react";
|
||||
|
||||
|
|
@ -74,14 +74,21 @@ export async function persistNotification(notification: NotificationData) {
|
|||
signals.forEach(x => x());
|
||||
}
|
||||
|
||||
export async function deleteNotification(timestamp: number) {
|
||||
const log = await getLog();
|
||||
const index = log.findIndex(x => x.timestamp === timestamp);
|
||||
if (index === -1) return;
|
||||
export async function deleteNotification(id: string) {
|
||||
let found = false;
|
||||
|
||||
log.splice(index, 1);
|
||||
await DataStore.set(KEY, log);
|
||||
signals.forEach(x => x());
|
||||
await DataStore.update(KEY, (old: PersistentNotificationData[] | undefined) => {
|
||||
const log = old ?? [];
|
||||
const index = log.findIndex(x => x.id === id);
|
||||
if (index === -1) return log;
|
||||
|
||||
log.splice(index, 1);
|
||||
found = true;
|
||||
return log;
|
||||
});
|
||||
|
||||
if (found)
|
||||
signals.forEach(x => x());
|
||||
}
|
||||
|
||||
export function useLogs() {
|
||||
|
|
@ -113,7 +120,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
|||
if (removing) return;
|
||||
setRemoving(true);
|
||||
|
||||
setTimeout(() => deleteNotification(data.timestamp), 200);
|
||||
setTimeout(() => deleteNotification(data.id), 200);
|
||||
}}
|
||||
richBody={
|
||||
<div className={cl("body-wrapper")}>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import { Settings, SettingsStore } from "@api/Settings";
|
||||
import { createAndAppendStyle } from "@utils/css";
|
||||
import { isNonNullish } from "@utils/guards";
|
||||
import { ThemeStore } from "@vencord/discord-types";
|
||||
import { PopoutWindowStore } from "@webpack/common";
|
||||
|
||||
|
|
@ -42,6 +43,9 @@ async function toggle(isEnabled: boolean) {
|
|||
style.disabled = !isEnabled;
|
||||
}
|
||||
|
||||
// for cleanup
|
||||
let previousThemeBlobObjectURLs = [] as string[];
|
||||
|
||||
async function initThemes() {
|
||||
themesStyle ??= createAndAppendStyle("vencord-themes", userStyleRootNode);
|
||||
|
||||
|
|
@ -66,12 +70,18 @@ async function initThemes() {
|
|||
.filter(link => link !== null);
|
||||
|
||||
if (IS_WEB) {
|
||||
for (const theme of enabledThemes) {
|
||||
previousThemeBlobObjectURLs.forEach(url => URL.revokeObjectURL(url));
|
||||
|
||||
const objectUrls = await Promise.all(enabledThemes.map(async theme => {
|
||||
const themeData = await VencordNative.themes.getThemeData(theme);
|
||||
if (!themeData) continue;
|
||||
if (!themeData) return null;
|
||||
|
||||
const blob = new Blob([themeData], { type: "text/css" });
|
||||
links.push(URL.createObjectURL(blob));
|
||||
}
|
||||
return URL.createObjectURL(blob);
|
||||
}));
|
||||
|
||||
previousThemeBlobObjectURLs = objectUrls.filter(isNonNullish);
|
||||
links.push(...previousThemeBlobObjectURLs);
|
||||
} else {
|
||||
const localThemes = enabledThemes.map(theme => `vencord:///themes/${theme}?v=${Date.now()}`);
|
||||
links.push(...localThemes);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ interface Props extends React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLA
|
|||
}
|
||||
|
||||
export function Link(props: React.PropsWithChildren<Props>) {
|
||||
props = { ...props };
|
||||
|
||||
if (props.disabled) {
|
||||
props.style ??= {};
|
||||
props.style.pointerEvents = "none";
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@ export interface QuickActionProps {
|
|||
text: ReactNode;
|
||||
action?: () => void;
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function QuickAction(props: QuickActionProps) {
|
||||
const { Icon, action, text, disabled } = props;
|
||||
const { Icon, action, text, disabled, style } = props;
|
||||
|
||||
return (
|
||||
<button className={cl("pill")} onClick={action} disabled={disabled}>
|
||||
<button className={cl("pill")} onClick={action} disabled={disabled} style={style}>
|
||||
<Icon className={cl("img")} />
|
||||
{text}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { Margins } from "@utils/margins";
|
|||
import { classes } from "@utils/misc";
|
||||
import { useAwaiter, useCleanupEffect } from "@utils/react";
|
||||
import { PluginTag, PluginTags } from "@utils/types";
|
||||
import { Button, ConfirmModal,lodash, openModal, Parser, React, SearchableSelect, Select, TextInput, Tooltip, useMemo, useRef, useState } from "@webpack/common";
|
||||
import { Button, ConfirmModal, lodash, openModal, Parser, React, SearchableSelect, Select, TextInput, Tooltip, useMemo, useRef, useState } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
import Plugins, { ExcludedPlugins, PluginMeta } from "~plugins";
|
||||
|
|
@ -133,10 +133,10 @@ function PluginSettings() {
|
|||
<>
|
||||
<p>The following plugins require a restart:</p>
|
||||
<div>{changes.map((s, i) => (
|
||||
<>
|
||||
<React.Fragment key={s}>
|
||||
{i > 0 && ", "}
|
||||
{Parser.parse("`" + s.split(".")[0] + "`")}
|
||||
</>
|
||||
</React.Fragment>
|
||||
))}</div>
|
||||
</>
|
||||
</ConfirmModal>
|
||||
|
|
|
|||
|
|
@ -43,13 +43,7 @@ function onLocalThemeChange(fileName: string, value: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
async function onFileUpload(e: SyntheticEvent<HTMLInputElement>) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!e.currentTarget?.files?.length) return;
|
||||
const { files } = e.currentTarget;
|
||||
|
||||
async function doUploadThemes(files: ArrayLike<File>) {
|
||||
const uploads = Array.from(files, file => {
|
||||
const { name } = file;
|
||||
if (!name.endsWith(".css")) return;
|
||||
|
|
@ -68,6 +62,47 @@ async function onFileUpload(e: SyntheticEvent<HTMLInputElement>) {
|
|||
await Promise.all(uploads);
|
||||
}
|
||||
|
||||
async function onFileUpload(e: SyntheticEvent<HTMLInputElement>) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!e.currentTarget?.files?.length) return;
|
||||
await doUploadThemes(e.currentTarget.files);
|
||||
}
|
||||
|
||||
function useDropFile(refreshThemes: Function) {
|
||||
useEffect(() => {
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
if (!e.dataTransfer?.items.length) return;
|
||||
if (!Array.from(e.dataTransfer.items).some(item => item.kind === "file" && item.getAsFile()?.name.endsWith(".css")))
|
||||
return;
|
||||
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
};
|
||||
|
||||
const onDrop = async (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!e.dataTransfer?.files.length) return;
|
||||
|
||||
await doUploadThemes(
|
||||
Array.from(e.dataTransfer.files).filter(file => file.name.endsWith(".css"))
|
||||
);
|
||||
|
||||
refreshThemes();
|
||||
};
|
||||
|
||||
window.addEventListener("dragover", onDragOver);
|
||||
window.addEventListener("drop", onDrop);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("dragover", onDragOver);
|
||||
window.removeEventListener("drop", onDrop);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function LocalThemesTab() {
|
||||
const settings = useSettings(["enabledThemes"]);
|
||||
|
||||
|
|
@ -79,6 +114,9 @@ export function LocalThemesTab() {
|
|||
refreshLocalThemes();
|
||||
}, []);
|
||||
|
||||
// This condition is compile time so conditional hook is okay
|
||||
if (IS_WEB) useDropFile(refreshLocalThemes);
|
||||
|
||||
async function refreshLocalThemes() {
|
||||
const themes = await VencordNative.themes.getThemesList();
|
||||
setUserThemes(themes);
|
||||
|
|
@ -109,7 +147,7 @@ export function LocalThemesTab() {
|
|||
(
|
||||
<QuickAction
|
||||
text={
|
||||
<span style={{ position: "relative" }}>
|
||||
<span>
|
||||
Upload Theme
|
||||
<FileInput
|
||||
ref={fileInputRef}
|
||||
|
|
@ -123,6 +161,7 @@ export function LocalThemesTab() {
|
|||
</span>
|
||||
}
|
||||
Icon={PlusIcon}
|
||||
style={{ position: "relative" }}
|
||||
/>
|
||||
) : (
|
||||
<QuickAction
|
||||
|
|
|
|||
|
|
@ -16,14 +16,16 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./ipcMain";
|
||||
|
||||
import { app, net, protocol } from "electron";
|
||||
import { join } from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
import { initCsp } from "./csp";
|
||||
import { ensureSafePath } from "./ipcMain";
|
||||
import { RendererSettings } from "./settings";
|
||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||
import { ensureSafePath } from "./utils/ensureSafePath";
|
||||
import { installExt } from "./utils/extensions";
|
||||
|
||||
if (IS_VESKTOP || !IS_VANILLA) {
|
||||
|
|
|
|||
|
|
@ -27,11 +27,12 @@ import monacoHtml from "file://monacoWin.html?minify&base64";
|
|||
import { FSWatcher, mkdirSync, readFileSync, watch, writeFileSync } from "fs";
|
||||
import { open, readdir, readFile } from "fs/promises";
|
||||
import { release } from "os";
|
||||
import { join, normalize } from "path";
|
||||
import { join } from "path";
|
||||
|
||||
import { registerCspIpcHandlers } from "./csp/manager";
|
||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
||||
import { ALLOWED_PROTOCOLS, QUICK_CSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants";
|
||||
import { ensureSafePath } from "./utils/ensureSafePath";
|
||||
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||
|
||||
const RENDERER_CSS_PATH = join(__dirname, IS_VESKTOP ? "vencordDesktopRenderer.css" : "renderer.css");
|
||||
|
|
@ -40,13 +41,6 @@ mkdirSync(THEMES_DIR, { recursive: true });
|
|||
|
||||
registerCspIpcHandlers();
|
||||
|
||||
export function ensureSafePath(basePath: string, path: string) {
|
||||
const normalizedBasePath = normalize(basePath + "/");
|
||||
const newPath = join(basePath, path);
|
||||
const normalizedPath = normalize(newPath);
|
||||
return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;
|
||||
}
|
||||
|
||||
function readCss() {
|
||||
return readFile(QUICK_CSS_PATH, "utf-8").catch(() => "");
|
||||
}
|
||||
|
|
@ -86,7 +80,8 @@ ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
|||
if (!ALLOWED_PROTOCOLS.includes(protocol))
|
||||
throw "Disallowed protocol.";
|
||||
|
||||
shell.openExternal(url);
|
||||
shell.openExternal(url)
|
||||
.catch(err => console.error("[Vencord] Failed to open external link", url, err));
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -112,7 +107,11 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => {
|
|||
ipcMain.handle(IpcEvents.OPEN_THEMES_FOLDER, () => shell.openPath(THEMES_DIR));
|
||||
ipcMain.handle(IpcEvents.OPEN_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR));
|
||||
|
||||
let fsWatchers = [] as FSWatcher[];
|
||||
|
||||
ipcMain.handle(IpcEvents.INIT_FILE_WATCHERS, ({ sender }) => {
|
||||
fsWatchers.forEach(w => w.close());
|
||||
|
||||
let quickCssWatcher: FSWatcher | undefined;
|
||||
let rendererCssWatcher: FSWatcher | undefined;
|
||||
|
||||
|
|
@ -133,10 +132,13 @@ ipcMain.handle(IpcEvents.INIT_FILE_WATCHERS, ({ sender }) => {
|
|||
});
|
||||
}
|
||||
|
||||
fsWatchers = [quickCssWatcher, themesWatcher, rendererCssWatcher].filter(Boolean) as FSWatcher[];
|
||||
|
||||
sender.once("destroyed", () => {
|
||||
quickCssWatcher?.close();
|
||||
themesWatcher.close();
|
||||
rendererCssWatcher?.close();
|
||||
fsWatchers = [];
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
16
src/main/utils/ensureSafePath.ts
Normal file
16
src/main/utils/ensureSafePath.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { join, normalize } from "path";
|
||||
|
||||
export function ensureSafePath(basePath: string, path: string) {
|
||||
const normalizedBasePath = normalize(basePath + "/");
|
||||
const newPath = join(basePath, path);
|
||||
const normalizedPath = normalize(newPath);
|
||||
return normalizedPath === normalize(basePath) || normalizedPath.startsWith(normalizedBasePath)
|
||||
? normalizedPath
|
||||
: null;
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import { join } from "path";
|
|||
|
||||
import { DATA_DIR } from "./constants";
|
||||
import { crxToZip } from "./crxToZip";
|
||||
import { ensureSafePath } from "./ensureSafePath";
|
||||
import { fetchBuffer } from "./http";
|
||||
|
||||
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
|
||||
|
|
@ -33,24 +34,35 @@ async function extract(data: Buffer, outDir: string) {
|
|||
return new Promise<void>((resolve, reject) => {
|
||||
unzip(data, (err, files) => {
|
||||
if (err) return void reject(err);
|
||||
|
||||
Promise.all(Object.keys(files).map(async f => {
|
||||
// Signature stuff
|
||||
// 'Cannot load extension with file or directory name
|
||||
// _metadata. Filenames starting with "_" are reserved for use by the system.';
|
||||
if (f.startsWith("_metadata/")) return;
|
||||
|
||||
if (f.endsWith("/")) return void mkdir(join(outDir, f), { recursive: true });
|
||||
if (f.includes("\0")) throw new Error(`Invalid filename: "${f}"`);
|
||||
|
||||
if (f.endsWith("/")) {
|
||||
const dir = ensureSafePath(outDir, f);
|
||||
if (!dir) throw new Error(`Path traversal detected: "${f}"`);
|
||||
return void await mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const pathElements = f.split("/");
|
||||
const name = pathElements.pop()!;
|
||||
const directories = pathElements.join("/");
|
||||
const dir = join(outDir, directories);
|
||||
const directories = pathElements.slice(0, -1).join("/");
|
||||
|
||||
const dir = ensureSafePath(outDir, directories);
|
||||
if (!dir) throw new Error(`Path traversal detected: "${f}"`);
|
||||
|
||||
const filePath = ensureSafePath(outDir, f);
|
||||
if (!filePath) throw new Error(`Path traversal detected: "${f}"`);
|
||||
|
||||
if (directories) {
|
||||
await mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
await writeFile(join(dir, name), files[f]);
|
||||
await writeFile(filePath, files[f]);
|
||||
}))
|
||||
.then(() => resolve())
|
||||
.catch(err => {
|
||||
|
|
@ -79,5 +91,9 @@ export async function installExt(id: string) {
|
|||
.catch(err => console.error(`Failed to extract extension ${id}`, err));
|
||||
}
|
||||
|
||||
session.defaultSession.loadExtension(extDir);
|
||||
if (session.defaultSession.extensions) {
|
||||
session.defaultSession.extensions.loadExtension(id);
|
||||
} else {
|
||||
session.defaultSession.loadExtension(extDir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { isPluginEnabled } from "@api/PluginManager";
|
||||
import ExpressionClonerPlugin from "@plugins/expressionCloner";
|
||||
import { Devs } from "@utils/constants";
|
||||
|
|
@ -68,9 +68,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (
|
|||
const sticker = message.stickerItems.find(s => s.id === favoriteableId);
|
||||
if (!sticker?.format_type) return;
|
||||
|
||||
const idx = children.findIndex(c => Array.isArray(c) && findGroupChildrenByChildId("vc-copy-sticker-url", c) != null);
|
||||
|
||||
children.splice(idx, 0, buildMenuItem(sticker, idx !== -1));
|
||||
children.splice(-1, 0, buildMenuItem(sticker, false));
|
||||
};
|
||||
|
||||
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import "./styles.css";
|
||||
|
||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage } from "@api/Commands";
|
||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands";
|
||||
import { migratePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { sendMessage } from "@utils/discord";
|
||||
|
|
@ -96,11 +96,12 @@ export default definePlugin({
|
|||
tags: ["Commands", "Customisation", "Utility"],
|
||||
settings,
|
||||
|
||||
async start() {
|
||||
const tags = getTags();
|
||||
for (const tagName in tags) {
|
||||
registerTagCommand(tags[tagName]);
|
||||
}
|
||||
start() {
|
||||
getTags().forEach(registerTagCommand);
|
||||
},
|
||||
|
||||
stop() {
|
||||
getTags().forEach(tag => unregisterCommand(tag.name));
|
||||
},
|
||||
|
||||
commands: [
|
||||
|
|
@ -160,7 +161,7 @@ export default definePlugin({
|
|||
}
|
||||
|
||||
case "list": {
|
||||
const content = Object.values(getTags())
|
||||
const content = getTags()
|
||||
.map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`)
|
||||
.join("\n");
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export interface Tag {
|
|||
}
|
||||
|
||||
export function getTags() {
|
||||
return settings.store.tagsList;
|
||||
return Object.values(settings.store.tagsList);
|
||||
}
|
||||
|
||||
export function getTag(name: string) {
|
||||
|
|
|
|||
|
|
@ -139,7 +139,10 @@ function SelectSetting<T>({ settingsKey, label, options, disabled }: SelectOptio
|
|||
options={options}
|
||||
maxVisibleItems={5}
|
||||
closeOnSelect={true}
|
||||
select={v => settings.store[settingsKey] = v}
|
||||
select={v => {
|
||||
settings.store[settingsKey] = v;
|
||||
updateRPC();
|
||||
}}
|
||||
isSelected={v => v === settings.store[settingsKey]}
|
||||
serialize={v => String(v)}
|
||||
isDisabled={disabled}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,6 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
|
|||
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
|
||||
onClick={() => {
|
||||
const { enabled, oldThumb, oldTitle } = embed.dearrow;
|
||||
settings.store.dearrowByDefault = !enabled;
|
||||
embed.dearrow.enabled = !enabled;
|
||||
if (oldTitle) {
|
||||
embed.dearrow.oldTitle = embed.rawTitle;
|
||||
|
|
|
|||
|
|
@ -50,14 +50,14 @@ app.on("browser-window-created", (_, win) => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
RendererSettings.addChangeListener("plugins.FixSpotifyEmbeds.volume", newVolume => {
|
||||
try {
|
||||
cleanUpAndGetSpotifyFrames().forEach(frame =>
|
||||
frame.executeJavaScript(`globalThis._vcVolume = ${newVolume / 100}`)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("FixSpotifyEmbeds: Failed to update volume", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
RendererSettings.addChangeListener("plugins.FixSpotifyEmbeds.volume", newVolume => {
|
||||
try {
|
||||
cleanUpAndGetSpotifyFrames().forEach(frame =>
|
||||
frame.executeJavaScript(`globalThis._vcVolume = ${newVolume / 100}`)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("FixSpotifyEmbeds: Failed to update volume", e);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export default definePlugin({
|
|||
sendBotMessage(ctx.channel.id, {
|
||||
content: `
|
||||
discord.gg/${invite.code} ·
|
||||
Expires: <t:${new Date(invite.expires_at).getTime() / 1000}:R> ·
|
||||
Expires: <t:${Math.round(new Date(invite.expires_at).getTime() / 1000)}:R> ·
|
||||
Max uses: \`${invite.max_uses}\`
|
||||
`.trim().replace(/\s+/g, " ")
|
||||
});
|
||||
|
|
@ -55,7 +55,7 @@ export default definePlugin({
|
|||
const friendInviteList = invites.map(i =>
|
||||
`
|
||||
_discord.gg/${i.code}_ ·
|
||||
Expires: <t:${new Date(i.expires_at).getTime() / 1000}:R> ·
|
||||
Expires: <t:${Math.round(new Date(i.expires_at).getTime() / 1000)}:R> ·
|
||||
Times used: \`${i.uses}/${i.max_uses}\`
|
||||
`.trim().replace(/\s+/g, " ")
|
||||
);
|
||||
|
|
|
|||
|
|
@ -57,16 +57,44 @@ export default definePlugin({
|
|||
|
||||
videoClone.loop = settings.store.loop;
|
||||
videoClone.style.display = "none";
|
||||
videoClone.onleavepictureinpicture = () => videoClone.remove();
|
||||
|
||||
function launchPiP() {
|
||||
videoClone.currentTime = video.currentTime;
|
||||
videoClone.requestPictureInPicture();
|
||||
video.pause();
|
||||
videoClone.play();
|
||||
let cleaned = false;
|
||||
function cleanup() {
|
||||
if (cleaned) return;
|
||||
cleaned = true;
|
||||
|
||||
const { currentTime } = videoClone;
|
||||
|
||||
videoClone.onloadedmetadata = null;
|
||||
videoClone.onleavepictureinpicture = null;
|
||||
|
||||
videoClone.pause();
|
||||
videoClone.removeAttribute("src");
|
||||
videoClone.load();
|
||||
videoClone.remove();
|
||||
|
||||
// resume original if still in the document
|
||||
if (video.isConnected) {
|
||||
video.currentTime = currentTime;
|
||||
video.play().catch(() => 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (videoClone.readyState === 4 /* HAVE_ENOUGH_DATA */)
|
||||
videoClone.onleavepictureinpicture = cleanup;
|
||||
|
||||
async function launchPiP() {
|
||||
try {
|
||||
videoClone.currentTime = video.currentTime;
|
||||
video.pause();
|
||||
await videoClone.play();
|
||||
await videoClone.requestPictureInPicture();
|
||||
} catch (err) {
|
||||
console.error("Failed to enter Picture-in-Picture", err);
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
if (videoClone.readyState >= 1)
|
||||
launchPiP();
|
||||
else
|
||||
videoClone.onloadedmetadata = launchPiP;
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ import definePlugin, { IconComponent, StartAt } from "@utils/types";
|
|||
import { CloudUpload, MessageAttachment } from "@vencord/discord-types";
|
||||
import { DraftStore, DraftType, UploadAttachmentStore, UserStore, useStateFromStores } from "@webpack/common";
|
||||
|
||||
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);
|
||||
const objectURLMap = new Map<string, string[]>();
|
||||
|
||||
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);
|
||||
|
||||
const getImageBox = (url: string): Promise<{ width: number, height: number; } | null> =>
|
||||
new Promise(res => {
|
||||
|
|
@ -44,6 +45,8 @@ const getAttachments = async (channelId: string) =>
|
|||
UploadAttachmentStore.getUploads(channelId, DraftType.ChannelMessage)
|
||||
.map(async (upload: CloudUpload) => {
|
||||
const { isImage, filename, spoiler, item: { file } } = upload;
|
||||
|
||||
// FIXME: revoke object url to fix memory leak
|
||||
const url = URL.createObjectURL(file);
|
||||
const attachment: MessageAttachment = {
|
||||
id: generateId(),
|
||||
|
|
@ -59,13 +62,13 @@ const getAttachments = async (channelId: string) =>
|
|||
|
||||
if (isImage) {
|
||||
const box = await getImageBox(url);
|
||||
if (!box) return attachment;
|
||||
|
||||
attachment.width = box.width;
|
||||
attachment.height = box.height;
|
||||
if (box) {
|
||||
attachment.width = box.width;
|
||||
attachment.height = box.height;
|
||||
}
|
||||
}
|
||||
|
||||
return attachment;
|
||||
return { attachment, objectURL: url };
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -99,15 +102,20 @@ const PreviewButton: ChatBarButtonFactory = ({ isAnyChat, isEmpty, type: { attac
|
|||
return (
|
||||
<ChatBarButton
|
||||
tooltip="Preview Message"
|
||||
onClick={async () =>
|
||||
sendBotMessage(
|
||||
onClick={async () => {
|
||||
const attachments = hasAttachments ? await getAttachments(channelId) : undefined;
|
||||
const message = sendBotMessage(
|
||||
channelId,
|
||||
{
|
||||
content: getDraft(channelId),
|
||||
author: UserStore.getCurrentUser(),
|
||||
attachments: hasAttachments ? await getAttachments(channelId) : undefined,
|
||||
attachments: attachments?.map(a => a.attachment),
|
||||
}
|
||||
)}
|
||||
);
|
||||
|
||||
if (attachments)
|
||||
objectURLMap.set(message.id, attachments.map(a => a.objectURL));
|
||||
}}
|
||||
buttonProps={{
|
||||
style: {
|
||||
translate: "0 2px"
|
||||
|
|
@ -132,5 +140,15 @@ export default definePlugin({
|
|||
chatBarButton: {
|
||||
icon: PreviewIcon,
|
||||
render: PreviewButton
|
||||
},
|
||||
|
||||
flux: {
|
||||
MESSAGE_DELETE({ id: messageId }) {
|
||||
const objectURLs = objectURLMap.get(messageId);
|
||||
if (objectURLs) {
|
||||
objectURLs.forEach(url => URL.revokeObjectURL(url));
|
||||
objectURLMap.delete(messageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ export default function ReviewComponent({ review, refetch, profileId }: { review
|
|||
<>
|
||||
{Parser.parseGuildEventDescription(review.comment.substring(0, 200))}...
|
||||
<br />
|
||||
<a onClick={() => setShowAll(true)}>Read more</a>]
|
||||
<a onClick={() => setShowAll(true)}>Read more</a>
|
||||
</>
|
||||
)
|
||||
: Parser.parseGuildEventDescription(review.comment)}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (
|
|||
|
||||
const menuGroup = findGroupChildrenByChildId("delete", children);
|
||||
const deleteIndex = menuGroup?.findIndex(i => i?.props?.id === "delete");
|
||||
if (!deleteIndex || !menuGroup) return;
|
||||
if (deleteIndex == null || !menuGroup) return;
|
||||
|
||||
menuGroup.splice(deleteIndex - 1, 0, (
|
||||
<Menu.MenuItem
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export const VoiceRecorderWeb: VoiceRecorder = ({ setAudioBlob, onRecordingChang
|
|||
changeRecording(false);
|
||||
});
|
||||
recorder.stop();
|
||||
recorder.stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@ export default definePlugin({
|
|||
|
||||
stop() {
|
||||
socket.close();
|
||||
Native.closeSocket();
|
||||
},
|
||||
|
||||
settingsAboutComponent: () => (
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { createSocket, Socket } from "dgram";
|
||||
|
||||
let xsoSocket: Socket;
|
||||
let xsoSocket: Socket | null = null;
|
||||
|
||||
export function sendToOverlay(_, data: any) {
|
||||
data.messageType = data.type;
|
||||
|
|
@ -14,3 +14,8 @@ export function sendToOverlay(_, data: any) {
|
|||
xsoSocket ??= createSocket("udp4");
|
||||
xsoSocket.send(json, 42069, "127.0.0.1");
|
||||
}
|
||||
|
||||
export function closeSocket() {
|
||||
xsoSocket?.close();
|
||||
xsoSocket = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ export function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterO
|
|||
})
|
||||
.catch(error => {
|
||||
if (!isAlive) return;
|
||||
setState({ value: null, error, pending: false });
|
||||
setState({ value: opts.fallbackValue, error, pending: false });
|
||||
opts.onError?.(error);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -506,8 +506,8 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
|||
const isArrowFunction = originalFactoryCode.startsWith("(");
|
||||
|
||||
// 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
|
||||
let code = "0," + (!isArrowFunction ? "function" : "") + originalFactoryCode.slice(originalFactoryCode.indexOf("("));
|
||||
let patchedSource = code;
|
||||
let patchedCode = "0," + (!isArrowFunction ? "function" : "") + originalFactoryCode.slice(originalFactoryCode.indexOf("("));
|
||||
let patchedSource = patchedCode;
|
||||
let patchedFactory = originalFactory;
|
||||
|
||||
const patchedBy = new Set<string>();
|
||||
|
|
@ -520,70 +520,65 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
|||
|
||||
if (
|
||||
shouldCheckBuildNumber &&
|
||||
(patch.fromBuild != null && buildNumber < patch.fromBuild) ||
|
||||
(patch.toBuild != null && buildNumber > patch.toBuild)
|
||||
((patch.fromBuild != null && buildNumber < patch.fromBuild) || (patch.toBuild != null && buildNumber > patch.toBuild))
|
||||
) {
|
||||
patches.splice(i--, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const moduleMatches = typeof patch.find === "string"
|
||||
? code.includes(patch.find)
|
||||
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));
|
||||
? originalFactoryCode.includes(patch.find)
|
||||
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(originalFactoryCode));
|
||||
|
||||
if (!moduleMatches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save the result from the previous patch so we can restore it in case a patch group fails.
|
||||
const previousPatchedCode = patchedCode;
|
||||
const previousPatchedSource = patchedSource;
|
||||
const previousPatchedFactory = patchedFactory;
|
||||
|
||||
let shouldRestorePrevious = false;
|
||||
let markedAsPatched = false;
|
||||
|
||||
const executePatch = traceFunctionWithResults(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => {
|
||||
if (typeof match !== "string" && match.global) {
|
||||
match.lastIndex = 0;
|
||||
}
|
||||
|
||||
return code.replace(match, replace);
|
||||
return patchedCode.replace(match, replace);
|
||||
});
|
||||
|
||||
const previousCode = code;
|
||||
const previousFactory = originalFactory;
|
||||
let markedAsPatched = false;
|
||||
|
||||
// We change all patch.replacement to array in PluginManager
|
||||
for (const replacement of patch.replacement as PatchReplacement[]) {
|
||||
if (
|
||||
shouldCheckBuildNumber &&
|
||||
(replacement.fromBuild != null && buildNumber < replacement.fromBuild) ||
|
||||
(replacement.toBuild != null && buildNumber > replacement.toBuild)
|
||||
((replacement.fromBuild != null && buildNumber < replacement.fromBuild) || (replacement.toBuild != null && buildNumber > replacement.toBuild))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lastCode = code;
|
||||
const lastFactory = originalFactory;
|
||||
|
||||
let newPatchedCode: string = "";
|
||||
try {
|
||||
const [newCode, totalTime] = executePatch(replacement.match, replacement.replace as string);
|
||||
const [patchResult, totalTime] = executePatch(replacement.match, replacement.replace as string);
|
||||
newPatchedCode = patchResult;
|
||||
|
||||
if (IS_REPORTER) {
|
||||
patchTimings.push([patch.plugin, moduleId, replacement.match, totalTime]);
|
||||
}
|
||||
|
||||
if (newCode === code) {
|
||||
if (newPatchedCode === patchedCode) {
|
||||
if (!(patch.noWarn || replacement.noWarn)) {
|
||||
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(moduleId)}): ${replacement.match}`);
|
||||
if (IS_DEV) {
|
||||
logger.debug("Function Source:\n", code);
|
||||
logger.debug("Function Source:\n", patchedCode);
|
||||
}
|
||||
}
|
||||
|
||||
if (patch.group) {
|
||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
|
||||
code = previousCode;
|
||||
patchedFactory = previousFactory;
|
||||
|
||||
if (markedAsPatched) {
|
||||
patchedBy.delete(patch.plugin);
|
||||
}
|
||||
|
||||
shouldRestorePrevious = true;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -595,14 +590,17 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
|||
pluginsList.push(patch.plugin);
|
||||
}
|
||||
|
||||
code = newCode;
|
||||
patchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${pluginsList.join(", ")}\n${code}\n//# sourceURL=file:///WebpackModule${String(moduleId)}`;
|
||||
patchedFactory = (0, eval)(patchedSource);
|
||||
const newPatchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${pluginsList.join(", ")}\n${newPatchedCode}\n//# sourceURL=file:///WebpackModule${String(moduleId)}`;
|
||||
const newPatchedFactory = (0, eval)(newPatchedSource);
|
||||
|
||||
if (!patchedBy.has(patch.plugin)) {
|
||||
patchedBy.add(patch.plugin);
|
||||
markedAsPatched = true;
|
||||
}
|
||||
|
||||
patchedCode = newPatchedCode;
|
||||
patchedSource = newPatchedSource;
|
||||
patchedFactory = newPatchedFactory;
|
||||
} catch (err) {
|
||||
// FIXME: Maybe fix this properly
|
||||
const shouldSuppressError = patch.plugin === "ContextMenuAPI" && err instanceof SyntaxError && err.message.includes("arguments");
|
||||
|
|
@ -610,23 +608,25 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
|||
logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(moduleId)}): ${replacement.match}\n`, err);
|
||||
|
||||
if (IS_DEV) {
|
||||
diffErroredPatch(code, lastCode, lastCode.match(replacement.match)!);
|
||||
diffErroredPatch(newPatchedCode, patchedCode, patchedCode.match(replacement.match)!);
|
||||
}
|
||||
}
|
||||
|
||||
if (markedAsPatched) {
|
||||
patchedBy.delete(patch.plugin);
|
||||
}
|
||||
|
||||
if (patch.group) {
|
||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
|
||||
code = previousCode;
|
||||
patchedFactory = previousFactory;
|
||||
shouldRestorePrevious = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code = lastCode;
|
||||
patchedFactory = lastFactory;
|
||||
if (shouldRestorePrevious) {
|
||||
patchedCode = previousPatchedCode;
|
||||
patchedSource = previousPatchedSource;
|
||||
patchedFactory = previousPatchedFactory;
|
||||
|
||||
if (markedAsPatched) {
|
||||
patchedBy.delete(patch.plugin);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -648,9 +648,12 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
|||
function diffErroredPatch(code: string, lastCode: string, match: RegExpMatchArray) {
|
||||
const changeSize = code.length - lastCode.length;
|
||||
|
||||
const matchIndex = match?.index ?? 0;
|
||||
const matchLength = match?.[0]?.length ?? 0;
|
||||
|
||||
// Use 200 surrounding characters of context
|
||||
const start = Math.max(0, match.index! - 200);
|
||||
const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
|
||||
const start = Math.max(0, matchIndex - 200);
|
||||
const end = Math.min(lastCode.length, matchIndex + matchLength + 200);
|
||||
// (changeSize may be negative)
|
||||
const endPatched = end + changeSize;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue