/ * *
*
* @ 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 } ) ;
}
}