You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vizality/renderer/src/index.js

508 lines
16 KiB

/**
*
* @module Vizality
* @namespace Vizality
*/
import { log as _log, warn as _warn, error as _error, deprecate as _deprecate } from '@vizality/util/logger';
import { initialize as initializeWebpackModules, getModule, FluxDispatcher, getAllModules } from '@vizality/webpack';
import { Directories, Events, Protocols } from '@vizality/constants';
import { resolveCompiler } from '@vizality/compilers';
import { createElement } from '@vizality/util/dom';
import { toPlural } from '@vizality/util/string';
import { isArray } from '@vizality/util/array';
import { Updatable } from '@vizality/entities';
import { debounce } from 'lodash';
import { promisify } from 'util';
import cp from 'child_process';
import { promises } from 'fs';
import { join } from 'path';
const { readFile, writeFile } = promises;
const exec = promisify(cp.exec);
/**
* @extends Updatable
* @extends Events
*/
export default class Vizality extends Updatable {
constructor () {
super(Directories.ROOT, '', 'vizality');
/**
* This is a leftover prop from Updatable, not needed for Vizality's core.
*/
delete this.addonId;
/**
* @note Copy over VizalityNative to vizality.native and then delete
* VizalityNative, so that we are staying consistent and only have
* one top level global variable.
*/
this.native = window.VizalityNative;
delete window.VizalityNative;
/**
* Set up the git info, defaulting to ???
*/
this.git = {
upstream: '???',
branch: '???',
revision: '???'
};
/**
* Labels used for console logging purposes.
*/
this._labels = [ 'Vizality', 'Core' ];
this.initialize();
}
async handleConnectionOpen () {
return new Promise(resolve => {
if (getAllModules()?.length > 7000) {
return resolve();
}
FluxDispatcher.subscribe('CONNECTION_OPEN', () => resolve());
});
}
async ensureWebpackModules () {
try {
/**
* Initialize the webpack modules.
*/
await initializeWebpackModules();
await this.handleConnectionOpen();
} catch (err) {
this.error(`Something went wrong while initializing webpack modules: ${err}`);
}
}
/**
* Initialize Vizality.
*/
async initialize () {
try {
await this.ensureWebpackModules();
/**
* Set up a connectStoresAsync Flux method.
* @note This has to be after webpack modules have been initialized.
*/
const Flux = await getModule('Store', 'PersistedStore', true);
Flux.connectStoresAsync = (stores, fn) => Component =>
import('./components').AsyncComponent.from((async () => {
const awaitedStores = await Promise.all(stores);
return Flux.connectStores(awaitedStores, props => fn(awaitedStores, props))(Component);
})());
/**
* Get rid of Discord's "Hold Up" dev tools warning.
*/
DiscordNative?.window?.setDevtoolsCallbacks(null, null);
/**
* Instantiate the managers.
* @note We're doing this down here so that we can utilize webpack modules and
* components inside of the managers.
*/
this.manager = {};
const managers = [ 'API', 'Builtin', 'Plugin', 'Theme', 'Community' ];
for (const manager of managers) {
/**
* Make the manager names on the global object plural, except for Community.
*/
const formatted = manager === 'Community' ? manager : toPlural(manager);
this.manager[formatted.toLowerCase()] = new (await import(`./managers/${manager}`))();
}
await this.start();
/**
* Get and assign the newly updated git info.
*/
this.git = await this.manager.builtins.get('updater')?.getGitInfo();
/**
* Token manipulation stuff. Helps prevent unwanted logouts.
*/
if (this.settings.get('hideToken', true)) {
const tokenModule = getModule('hideToken');
tokenModule.hideToken = () => void 0;
setImmediate(() => tokenModule.showToken());
}
/**
* Enables/disables Discord Experiments.
*/
if (this.settings.get('discordExperiments', false)) {
const experimentsModule = getModule(user => user.isDeveloper !== void 0);
Object.defineProperty(experimentsModule, 'isDeveloper', {
get: () => true,
configurable: true
});
}
/**
* Trigger an event indicating that Vizality has been initialized.
*/
this.emit(Events.VIZALITY_READY);
} catch (err) {
return this.error(err);
}
}
/**
* Starts up the core functionality of Vizality, including APIs, builtins, plugins, and themes.
*/
async start () {
/**
* Clean up console by clearing it first, then log our startup banner.
*/
console.clear();
console.log('%c ', `background: url('${Protocols.ASSETS}/images/console-banner.gif') no-repeat center / contain; padding: 110px 350px; font-size: 1px; margin: 10px 0;`);
/**
* Set up the modules for the global vizality object.
*/
this.modules = {};
const modules = await import('./modules');
Object.assign(this.modules, modules);
/**
* Set up a shorthand for Vizality's Discord module.
* Make sure it doesn't exist already, just in case Discord ever uses the same global namespace itself.
*/
if (!window.$discord) {
const discord = await import('./modules/discord');
window.$discord = Object.assign({}, discord);
window.$discord.constants = Object.assign({}, { ...window.$discord.constants, ...window.$discord.constants.default });
}
/**
* Perform some cleanup, removing unnecessary properties.
*/
delete this.modules.discord;
delete window.$discord.constants.default;
delete window.$discord.default;
this.deprecate('The global namespace object "discord" will be removed on May 24, 2022. Please use "$discord" instead.');
this.deprecate('The global namespace object "vizality" will be removed on May 24, 2022. Please use "$vz" instead. "$vz" has a slightly different data structure, but provides the same information.');
/**
* Initialize the APIs.
*/
this.api = {};
await this.manager.apis.initialize();
/**
* Set up a shorthand vizality global object with the namespace $vz.
*/
window.$vz = Object.assign({}, this.manager, this.modules);
window.$vz.api = this.api;
/**
* Set up and initialize Vizality's core settings.
*/
this.settings = this.api.settings._buildCategoryObject('settings');
window.$vz.settings = this.settings;
this.settings.set('developer', false);
/**
* Check if the current user is a Vizality Developer.
* @note This is going before the settings ready event below, because we check this in
* the Icon component after settings ready event has triggered.
*/
/*
* if (Developers.some(developer => developer.id === $discord.users.getCurrentUser()?.id)) {
* console.log('yes');
* this.settings.set('developer', true);
* } else {
* this.settings.set('developer', false);
* }
*/
/**
* Trigger an event indicating that Vizality's settings are ready.
*/
this.emit(Events.VIZALITY_SETTINGS_READY);
/**
* Inject core Vizality styles.
*/
this._injectCoreStyles();
/**
* Patch Discord's stylized console logs.
* @note This has to be after settings have been initialized.
*/
this._patchDiscordLogs();
/**
* Initialize builtins, plugins, and themes.
*/
await this.manager.builtins.initialize();
this.manager.themes.initialize();
this.manager.plugins.initialize();
this.manager.community.initialize();
}
/**
* Shuts down Vizality's APIs, builtins, plugins, and themes.
*/
async stop () {
/**
* Most importantly here is to stop entities in the order of plugins -> builtins -> apis
* to ensure there aren't any problems shutting down something that relies on
* something else.
*/
this.manager.themes.stop();
this.manager.plugins.stop();
await this.manager.builtins.stop();
await this.manager.apis.stop();
}
/**
*
*/
async _patchDiscordLogs () {
try {
const { setLogFn } = await getModule('setLogFn', true);
if (!this.settings.get('showDiscordConsoleLogs', false)) {
/**
* Removes Discord's logs entirely... except for the logs that don't use the
* setLogFn function (i.e. normal console methods).
*/
setLogFn(() => void 0);
} else {
/**
* Patch Discord's logs to adhere to Vizality's log styles.
*/
setLogFn((submodule, type, ...message) => {
switch (type) {
case 'info':
case 'log':
return _log({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message });
case 'error':
case 'trace':
return _error({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message });
case 'warn':
return _warn({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message });
default:
return _log({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message });
}
});
}
} catch (err) {
return this.error(err);
}
}
/**
*
*/
_patchWebSocket () {
const _this = this;
window.WebSocket = class PatchedWebSocket extends window.WebSocket {
constructor (url) {
super(url);
this.addEventListener('message', data => {
_this.emit(`webSocketMessage:${data.origin.slice(6)}`, data);
});
}
};
}
/**
*
* @param {boolean} [force=false] Whether to
* @returns {Promise<boolean>}
*/
async _update (force = false) {
try {
const success = await super._update(force);
/**
*
*/
if (success) {
try {
await exec('npm install --only=prod --legacy-peer-deps', { cwd: this.dir });
} catch (err) {
return this.error(`An error occurred while updating Vizality's dependencies!`, err);
}
/**
*
*/
if (!document.querySelector(`#vz-updater-update-complete, [vz-route='updater']`)) {
this.api.notifications.sendToast({
id: 'VIZALITY_UPDATER_UPDATE_COMPLETE',
header: 'Update complete!',
content: `Please click 'Reload' to complete the final stages of this Vizality update.`,
icon: 'CloudDone',
buttons: [
{
text: 'Reload',
color: 'green',
look: 'ghost',
onClick: () => DiscordNative?.app?.relaunch()
},
{
text: 'Postpone',
color: 'grey',
look: 'outlined',
onClick: () => this.api.notifications.closeToast('VIZALITY_UPDATER_UPDATE_COMPLETE')
}
]
});
}
this.manager.builtins.get('updater').settings.set('awaitingReload', true);
}
return success;
} catch (err) {
return this.error(`An error occurred while updating Vizality!`, err);
}
}
/**
* Injects a style element containing Vizality's core styles.
* @note Includes a file watcher and CSS file generator only for Vizality developers.
* @returns {Promise<void>}
*/
async _injectCoreStyles () {
try {
const id = 'vizality-core-styles';
/**
* Check if the user is a Vizality developer.
*/
if (this.settings.get('developer', false)) {
const path = join(Directories.STYLES, 'main.scss');
const compiler = resolveCompiler(path);
const style = createElement('style', { id, 'vz-style': '' });
document.head.appendChild(style);
/**
* Compiles the Sass and then writes it into a main.css file.
* @param {boolean} [showLogs=true] Whether to show log messages
* @returns {Promise<void>}
*/
const compile = debounce(async (showLogs = true) => {
try {
const before = performance.now();
const compiled = await compiler.compile();
style.innerHTML = compiled;
if (showLogs) {
const after = performance.now();
const time = parseFloat((after - before).toFixed()).toString().replace(/^0+/, '') || 0;
/**
* Let's format the milliseconds to seconds.
*/
let formattedTime = Math.round((time / 1000 + Number.EPSILON) * 100) / 100;
/**
* If it ends up being so fast that it rounds to 0, let's show formatting
* to 3 decimal places, otherwise show 2 decimal places.
*/
if (formattedTime === 0) {
formattedTime = Math.round((time / 1000 + Number.EPSILON) * 1000) / 1000;
}
/**
* If it is still 0, let's just say it's fast.
*/
if (formattedTime === 0) {
this.log(`Core styles compiled. Compilation was nearly instant!`);
} else {
this.log(`Core styles compiled. Compilation took ${formattedTime} seconds!`);
}
}
await writeFile(join(Directories.STYLES, 'main.css'), compiled);
} catch (err) {
return this.error(err);
}
}, 300);
/**
* Set up the watcher for the compiler.
*/
compiler.enableWatcher();
compiler.on('src-update', compile);
this[`__compileStylesheet_${id}`] = compile;
this[`__compiler_${id}`] = compiler;
return compile(false);
}
/**
* Create a style element and inject the CSS into it.
*/
const path = join(__dirname, 'styles', 'main.css');
const style = createElement('style', { id, 'vz-style': '' });
document.head.appendChild(style);
const css = await readFile(path, 'utf8');
style.innerHTML = css;
} catch (err) {
return this.error(err);
}
}
/**
*
* @param {any} message Message to log
* @returns {void}
*/
log (...message) {
// In case the addon wants to provide their own labels
if (isArray(message[0])) {
const _message = message.slice(1);
return _log({ labels: message[0], message: _message });
}
return _log({ labels: this._labels, message });
}
/**
*
* @param {any} message Message to log
* @returns {void}
*/
warn (...message) {
// In case the addon wants to provide their own labels
if (isArray(message[0])) {
const _message = message.slice(1);
return _warn({ labels: message[0], message: _message });
}
return _warn({ labels: this._labels, message });
}
/**
*
* @param {any} message Message to log
* @returns {void}
*/
error (...message) {
// In case the addon wants to provide their own labels
if (isArray(message[0])) {
const _message = message.slice(1);
return _error({ labels: message[0], message: _message });
}
return _error({ labels: this._labels, message });
}
/**
*
* @param {any} message Message to log
* @returns {void}
*/
deprecate (...message) {
// In case the addon wants to provide their own labels
if (isArray(message[0])) {
const _message = message.slice(1);
return _deprecate({ labels: message[0], message: _message });
}
return _deprecate({ labels: this._labels, message });
}
}