Add shortcut for lazy loading chunks
This commit is contained in:
parent
8fd5d068da
commit
ed5ae2ba5c
8 changed files with 191 additions and 162 deletions
|
@ -241,17 +241,26 @@ page.on("console", async e => {
|
||||||
error: await maybeGetError(e.args()[3]) ?? "Unknown error"
|
error: await maybeGetError(e.args()[3]) ?? "Unknown error"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "LazyChunkLoader:":
|
||||||
|
console.error(await getText());
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
case "A fatal error occurred:":
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "Reporter:":
|
case "Reporter:":
|
||||||
console.error(await getText());
|
console.error(await getText());
|
||||||
|
|
||||||
switch (message) {
|
switch (message) {
|
||||||
|
case "A fatal error occurred:":
|
||||||
|
process.exit(1);
|
||||||
case "Webpack Find Fail:":
|
case "Webpack Find Fail:":
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
report.badWebpackFinds.push(otherMessage);
|
report.badWebpackFinds.push(otherMessage);
|
||||||
break;
|
break;
|
||||||
case "A fatal error occurred:":
|
|
||||||
process.exit(1);
|
|
||||||
case "Finished test":
|
case "Finished test":
|
||||||
await browser.close();
|
await browser.close();
|
||||||
await printReport();
|
await printReport();
|
||||||
|
|
|
@ -129,7 +129,7 @@ export const SettingsStore = new SettingsStoreClass(settings, {
|
||||||
|
|
||||||
if (path === "plugins" && key in plugins)
|
if (path === "plugins" && key in plugins)
|
||||||
return target[key] = {
|
return target[key] = {
|
||||||
enabled: plugins[key].required ?? plugins[key].enabledByDefault ?? false
|
enabled: IS_REPORTER ?? plugins[key].required ?? plugins[key].enabledByDefault ?? false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||||
|
|
167
src/debug/loadLazyChunks.ts
Normal file
167
src/debug/loadLazyChunks.ts
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import { canonicalizeMatch } from "@utils/patches";
|
||||||
|
import * as Webpack from "@webpack";
|
||||||
|
import { wreq } from "@webpack";
|
||||||
|
|
||||||
|
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
|
||||||
|
|
||||||
|
export async function loadLazyChunks() {
|
||||||
|
try {
|
||||||
|
LazyChunkLoaderLogger.log("Loading all chunks...");
|
||||||
|
|
||||||
|
const validChunks = new Set<string>();
|
||||||
|
const invalidChunks = new Set<string>();
|
||||||
|
const deferredRequires = new Set<string>();
|
||||||
|
|
||||||
|
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
||||||
|
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
||||||
|
|
||||||
|
// True if resolved, false otherwise
|
||||||
|
const chunksSearchPromises = [] as Array<() => boolean>;
|
||||||
|
|
||||||
|
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);
|
||||||
|
|
||||||
|
async function searchAndLoadLazyChunks(factoryCode: string) {
|
||||||
|
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
|
||||||
|
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
|
||||||
|
|
||||||
|
// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
|
||||||
|
// the chunk containing the component
|
||||||
|
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
|
||||||
|
|
||||||
|
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
|
||||||
|
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
|
||||||
|
|
||||||
|
if (chunkIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let invalidChunkGroup = false;
|
||||||
|
|
||||||
|
for (const id of chunkIds) {
|
||||||
|
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
||||||
|
|
||||||
|
const isWasm = await fetch(wreq.p + wreq.u(id))
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
||||||
|
|
||||||
|
if (isWasm && IS_WEB) {
|
||||||
|
invalidChunks.add(id);
|
||||||
|
invalidChunkGroup = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validChunks.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!invalidChunkGroup) {
|
||||||
|
validChunkGroups.add([chunkIds, entryPoint]);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Loads all found valid chunk groups
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(validChunkGroups)
|
||||||
|
.map(([chunkIds]) =>
|
||||||
|
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Requires the entry points for all valid chunk groups
|
||||||
|
for (const [, entryPoint] of validChunkGroups) {
|
||||||
|
try {
|
||||||
|
if (shouldForceDefer) {
|
||||||
|
deferredRequires.add(entryPoint);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setImmediate to only check if all chunks were loaded after this function resolves
|
||||||
|
// We check if all chunks were loaded every time a factory is loaded
|
||||||
|
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
|
||||||
|
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
|
||||||
|
setTimeout(() => {
|
||||||
|
let allResolved = true;
|
||||||
|
|
||||||
|
for (let i = 0; i < chunksSearchPromises.length; i++) {
|
||||||
|
const isResolved = chunksSearchPromises[i]();
|
||||||
|
|
||||||
|
if (isResolved) {
|
||||||
|
// Remove finished promises to avoid having to iterate through a huge array everytime
|
||||||
|
chunksSearchPromises.splice(i--, 1);
|
||||||
|
} else {
|
||||||
|
allResolved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allResolved) chunksSearchingResolve();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Webpack.factoryListeners.add(factory => {
|
||||||
|
let isResolved = false;
|
||||||
|
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
|
||||||
|
|
||||||
|
chunksSearchPromises.push(() => isResolved);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const factoryId in wreq.m) {
|
||||||
|
let isResolved = false;
|
||||||
|
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
|
||||||
|
|
||||||
|
chunksSearchPromises.push(() => isResolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
await chunksSearchingDone;
|
||||||
|
|
||||||
|
// Require deferred entry points
|
||||||
|
for (const deferredRequire of deferredRequires) {
|
||||||
|
wreq!(deferredRequire as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
||||||
|
const allChunks = [] as string[];
|
||||||
|
|
||||||
|
// Matches "id" or id:
|
||||||
|
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
|
||||||
|
const id = currentMatch[1] ?? currentMatch[2];
|
||||||
|
if (id == null) continue;
|
||||||
|
|
||||||
|
allChunks.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
||||||
|
|
||||||
|
// Chunks that are not loaded (not used) by Discord code anymore
|
||||||
|
const chunksLeft = allChunks.filter(id => {
|
||||||
|
return !(validChunks.has(id) || invalidChunks.has(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(chunksLeft.map(async id => {
|
||||||
|
const isWasm = await fetch(wreq.p + wreq.u(id))
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
||||||
|
|
||||||
|
// Loads and requires a chunk
|
||||||
|
if (!isWasm) {
|
||||||
|
await wreq.e(id as any);
|
||||||
|
if (wreq.m[id]) wreq(id as any);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
LazyChunkLoaderLogger.log("Finished loading all chunks!");
|
||||||
|
} catch (e) {
|
||||||
|
LazyChunkLoaderLogger.log("A fatal error occurred:", e);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,171 +5,22 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { canonicalizeMatch } from "@utils/patches";
|
|
||||||
import * as Webpack from "@webpack";
|
import * as Webpack from "@webpack";
|
||||||
import { wreq } from "@webpack";
|
|
||||||
import { patches } from "plugins";
|
import { patches } from "plugins";
|
||||||
|
|
||||||
|
import { loadLazyChunks } from "./loadLazyChunks";
|
||||||
|
|
||||||
const ReporterLogger = new Logger("Reporter");
|
const ReporterLogger = new Logger("Reporter");
|
||||||
|
|
||||||
async function runReporter() {
|
async function runReporter() {
|
||||||
|
try {
|
||||||
ReporterLogger.log("Starting test...");
|
ReporterLogger.log("Starting test...");
|
||||||
|
|
||||||
try {
|
let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void;
|
||||||
const validChunks = new Set<string>();
|
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
|
||||||
const invalidChunks = new Set<string>();
|
|
||||||
const deferredRequires = new Set<string>();
|
|
||||||
|
|
||||||
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
|
||||||
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
await loadLazyChunksDone;
|
||||||
|
|
||||||
// True if resolved, false otherwise
|
|
||||||
const chunksSearchPromises = [] as Array<() => boolean>;
|
|
||||||
|
|
||||||
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);
|
|
||||||
|
|
||||||
async function searchAndLoadLazyChunks(factoryCode: string) {
|
|
||||||
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
|
|
||||||
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
|
|
||||||
|
|
||||||
// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
|
|
||||||
// the chunk containing the component
|
|
||||||
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
|
|
||||||
|
|
||||||
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
|
|
||||||
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
|
|
||||||
|
|
||||||
if (chunkIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let invalidChunkGroup = false;
|
|
||||||
|
|
||||||
for (const id of chunkIds) {
|
|
||||||
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
|
||||||
|
|
||||||
const isWasm = await fetch(wreq.p + wreq.u(id))
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
|
||||||
|
|
||||||
if (isWasm && IS_WEB) {
|
|
||||||
invalidChunks.add(id);
|
|
||||||
invalidChunkGroup = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
validChunks.add(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!invalidChunkGroup) {
|
|
||||||
validChunkGroups.add([chunkIds, entryPoint]);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Loads all found valid chunk groups
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(validChunkGroups)
|
|
||||||
.map(([chunkIds]) =>
|
|
||||||
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Requires the entry points for all valid chunk groups
|
|
||||||
for (const [, entryPoint] of validChunkGroups) {
|
|
||||||
try {
|
|
||||||
if (shouldForceDefer) {
|
|
||||||
deferredRequires.add(entryPoint);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setImmediate to only check if all chunks were loaded after this function resolves
|
|
||||||
// We check if all chunks were loaded every time a factory is loaded
|
|
||||||
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
|
|
||||||
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
|
|
||||||
setTimeout(() => {
|
|
||||||
let allResolved = true;
|
|
||||||
|
|
||||||
for (let i = 0; i < chunksSearchPromises.length; i++) {
|
|
||||||
const isResolved = chunksSearchPromises[i]();
|
|
||||||
|
|
||||||
if (isResolved) {
|
|
||||||
// Remove finished promises to avoid having to iterate through a huge array everytime
|
|
||||||
chunksSearchPromises.splice(i--, 1);
|
|
||||||
} else {
|
|
||||||
allResolved = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allResolved) chunksSearchingResolve();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Webpack.beforeInitListeners.add(async () => {
|
|
||||||
ReporterLogger.log("Loading all chunks...");
|
|
||||||
|
|
||||||
Webpack.factoryListeners.add(factory => {
|
|
||||||
let isResolved = false;
|
|
||||||
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
|
|
||||||
|
|
||||||
chunksSearchPromises.push(() => isResolved);
|
|
||||||
});
|
|
||||||
|
|
||||||
// setImmediate to only search the initial factories after Discord initialized the app
|
|
||||||
// our beforeInitListeners are called before Discord initializes the app
|
|
||||||
setTimeout(() => {
|
|
||||||
for (const factoryId in wreq.m) {
|
|
||||||
let isResolved = false;
|
|
||||||
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
|
|
||||||
|
|
||||||
chunksSearchPromises.push(() => isResolved);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
await chunksSearchingDone;
|
|
||||||
|
|
||||||
// Require deferred entry points
|
|
||||||
for (const deferredRequire of deferredRequires) {
|
|
||||||
wreq!(deferredRequire as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
|
||||||
const allChunks = [] as string[];
|
|
||||||
|
|
||||||
// Matches "id" or id:
|
|
||||||
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
|
|
||||||
const id = currentMatch[1] ?? currentMatch[2];
|
|
||||||
if (id == null) continue;
|
|
||||||
|
|
||||||
allChunks.push(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
|
||||||
|
|
||||||
// Chunks that are not loaded (not used) by Discord code anymore
|
|
||||||
const chunksLeft = allChunks.filter(id => {
|
|
||||||
return !(validChunks.has(id) || invalidChunks.has(id));
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(chunksLeft.map(async id => {
|
|
||||||
const isWasm = await fetch(wreq.p + wreq.u(id))
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
|
||||||
|
|
||||||
// Loads and requires a chunk
|
|
||||||
if (!isWasm) {
|
|
||||||
await wreq.e(id as any);
|
|
||||||
if (wreq.m[id]) wreq(id as any);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
ReporterLogger.log("Finished loading all chunks!");
|
|
||||||
|
|
||||||
for (const patch of patches) {
|
for (const patch of patches) {
|
||||||
if (!patch.all) {
|
if (!patch.all) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import definePlugin, { PluginNative, StartAt } from "@utils/types";
|
||||||
import * as Webpack from "@webpack";
|
import * as Webpack from "@webpack";
|
||||||
import { extract, filters, findAll, findModuleId, search } from "@webpack";
|
import { extract, filters, findAll, findModuleId, search } from "@webpack";
|
||||||
import * as Common from "@webpack/common";
|
import * as Common from "@webpack/common";
|
||||||
|
import { loadLazyChunks } from "debug/loadLazyChunks";
|
||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
|
|
||||||
const DESKTOP_ONLY = (f: string) => () => {
|
const DESKTOP_ONLY = (f: string) => () => {
|
||||||
|
@ -82,6 +83,7 @@ function makeShortcuts() {
|
||||||
wpsearch: search,
|
wpsearch: search,
|
||||||
wpex: extract,
|
wpex: extract,
|
||||||
wpexs: (code: string) => extract(findModuleId(code)!),
|
wpexs: (code: string) => extract(findModuleId(code)!),
|
||||||
|
loadLazyChunks: IS_DEV ? loadLazyChunks : () => { throw new Error("loadLazyChunks is dev only."); },
|
||||||
find,
|
find,
|
||||||
findAll: findAll,
|
findAll: findAll,
|
||||||
findByProps,
|
findByProps,
|
||||||
|
|
|
@ -44,7 +44,6 @@ const settings = Settings.plugins;
|
||||||
|
|
||||||
export function isPluginEnabled(p: string) {
|
export function isPluginEnabled(p: string) {
|
||||||
return (
|
return (
|
||||||
IS_REPORTER ||
|
|
||||||
Plugins[p]?.required ||
|
Plugins[p]?.required ||
|
||||||
Plugins[p]?.isDependency ||
|
Plugins[p]?.isDependency ||
|
||||||
settings[p]?.enabled
|
settings[p]?.enabled
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
|
||||||
import { FluxDispatcher } from "@webpack/common";
|
import { FluxDispatcher } from "@webpack/common";
|
||||||
|
|
||||||
const enum Intensity {
|
const enum Intensity {
|
||||||
|
@ -46,6 +46,7 @@ export default definePlugin({
|
||||||
name: "PartyMode",
|
name: "PartyMode",
|
||||||
description: "Allows you to use party mode cause the party never ends ✨",
|
description: "Allows you to use party mode cause the party never ends ✨",
|
||||||
authors: [Devs.UwUDev],
|
authors: [Devs.UwUDev],
|
||||||
|
reporterTestable: ReporterTestable.None,
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class Logger {
|
||||||
constructor(public name: string, public color: string = "white") { }
|
constructor(public name: string, public color: string = "white") { }
|
||||||
|
|
||||||
private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") {
|
private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") {
|
||||||
if (IS_REPORTER) {
|
if (IS_REPORTER && IS_WEB) {
|
||||||
console[level]("[Vencord]", this.name + ":", ...args);
|
console[level]("[Vencord]", this.name + ":", ...args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue