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

480 lines
15 KiB

/**
*
* @module Vizality
* @namespace Vizality
*/
import { initialize as initializeWebpackModules, getModule } from '@vizality/webpack';
import { log as _log, warn as _warn, error as _error } from '@vizality/util/logger';
import { Directories, Developers, Events, Protocols } from '@vizality/constants';
import { resolveCompiler } from '@vizality/compilers';
import { createElement } from '@vizality/util/dom';
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' ];
/**
* Wait for the document to be loaded and then initialize Vizality.
* @todo There should be a better way to do this, look into it more in the future.
*/
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.initialize());
} else {
this.initialize();
}
}
/**
* Initialize Vizality.
*/
async initialize () {
try {
/**
* Get rid of Discord's "Hold Up" dev tools warning.
*/
DiscordNative?.window?.setDevtoolsCallbacks(null, null);
/**
* Initialize the webpack modules.
*/
await initializeWebpackModules();
/**
* 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 =>
require('@vizality/components').AsyncComponent.from((async () => {
const awaitedStores = await Promise.all(stores);
// @todo Remember to add these to @discord/settings module (darkSiderbar, etc.): awaitedStores
return Flux.connectStores(awaitedStores, props => fn(awaitedStores, props))(Component);
})());
/**
* Instantiate the managers.
* @note We're doing this down here, and uglily so we can use webpack modules and
* Vizality's components module inside the managers.
*/
this.manager = {};
const CommunityManager = require('./managers/Community').default;
const BuiltinManager = require('./managers/Builtin').default;
const PluginManager = require('./managers/Plugin').default;
const ThemeManager = require('./managers/Theme').default;
const APIManager = require('./managers/API').default;
this.manager.community = new CommunityManager();
this.manager.builtins = new BuiltinManager();
this.manager.plugins = new PluginManager();
this.manager.themes = new ThemeManager();
this.manager.apis = new APIManager();
/**
* Time to start Vizality.
*/
const before = performance.now();
const startVizality = () => {
return new Promise(resolve => {
resolve(this.start());
});
};
await startVizality()
.then(() => {
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;
}
return this.log(`Vizality loaded. Startup took ${formattedTime} seconds!`);
});
/**
* 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());
}
/**
* 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;`);
/**
* Initialize the APIs.
*/
this.api = {};
await this.manager.apis.initialize();
/**
* Set up and initialize Vizality's core settings.
*/
this.settings = this.api.settings._buildCategoryObject('settings');
/**
* Trigger an event indicating that Vizality's settings are ready.
*/
this.emit(Events.VIZALITY_SETTINGS_READY);
/**
* Check if the current user is a Vizality Developer.
*/
const currentUserId = (await import('@discord/user')).getCurrentUser()?.id;
if (Developers.some(developer => developer.id === currentUserId)) {
this.settings.set('developer', true);
} else {
this.settings.set('developer', false);
}
/**
* Inject core Vizality styles.
*/
this._injectCoreStyles();
/**
* Patch Discord's stylized console logs.
* @note This has to be after settings have been initialized.
*/
this._patchDiscordLogs();
/**
* Set up the modules for the global vizality object.
*/
this.modules = {};
const modules = await import('@vizality/modules');
for (const mdl of Object.keys(modules)) {
Object.assign(this.modules, { [mdl]: modules[mdl] });
}
/**
* Set up a shorthand vizality global object with the namespace $vz.
*/
window.$vz = Object.assign({}, this.manager, this.api, this.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 discordModule = await import('@discord');
window.discord = Object.assign({}, discordModule);
delete window.discord.default;
}
/**
* 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 });
}
}