Skip to content

Commit

Permalink
feat(#131): hmr on commands and events files
Browse files Browse the repository at this point in the history
  • Loading branch information
barthofu committed Dec 8, 2023
1 parent 225f362 commit 69f6887
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 40 deletions.
2 changes: 2 additions & 0 deletions mikro-orm.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @ts-nocheck

import { mikroORMConfig } from "./src/configs/database"
import * as entities from "@entities"
import { PluginsManager } from "@services"
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
"install:plugins": "installoop --rootDir=./build/plugins",
"type:check": "tsc --pretty --skipLibCheck --noEmit",
"start": "cross-env NODE_ENV=production node build/main.js",
"dev": "cross-env NODE_ENV=development nodemon --exec node -r ts-node/register/transpile-only src/main.ts",
"dev:start": "cross-env NODE_ENV=production node -r ts-node/register/transpile-only src/main.ts",
"dev": "cross-env NODE_ENV=development nodemon --exec ts-node --transpile-only src/main.ts",
"i18n": "typesafe-i18n",
"migration:create": "npx mikro-orm migration:create",
"migration:up": "npx mikro-orm migration:up",
Expand Down Expand Up @@ -121,7 +120,8 @@
},
"nodemonConfig": {
"ignore": [
"src/i18n/**/!(i18n-types.ts)"
"src/i18n/**/!(i18n-types.ts)",
"src/{events,commands}/**/*.{ts,js}"
]
},
"tscord": {
Expand Down
92 changes: 83 additions & 9 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,91 @@
import 'dotenv/config'
import 'reflect-metadata'

import { importx } from "@discordx/importer"
import { resolve } from "@discordx/importer"
import chokidar from 'chokidar'
import discordLogs from "discord-logs"
import { Client, DIService, tsyringeDependencyRegistryEngine } from "discordx"
import { Client, DIService, MetadataStorage, tsyringeDependencyRegistryEngine } from "discordx"
import { container } from "tsyringe"

import { Server } from "@api/server"
import { apiConfig, generalConfig, websocketConfig } from "@configs"
import { NoBotTokenError } from "@errors"
import { RequestContext } from '@mikro-orm/core'
import { Database, ErrorHandler, EventManager, ImagesUpload, Logger, PluginsManager, Store, WebSocket } from "@services"
import { initDataTable, resolveDependency } from "@utils/functions"
import chalk from 'chalk'
import { clientConfig } from "./client"
import { RequestContext } from '@mikro-orm/core'

async function run() {
const importPattern = __dirname + "/{events,commands}/**/*.{ts,js}"

/**
* Import files
* @param path glob pattern
*/
async function loadFiles(path: string): Promise<void> {
const files = await resolve(path)
await Promise.all(
files.map((file) => {
const newFileName = file.replace('file://', '')
delete require.cache[newFileName]
import(newFileName)
})
)
}

/**
* Hot reload
*/
async function reload(client: Client) {

const store = await resolveDependency(Store)
store.set('botHasBeenReloaded', true)

const logger = await resolveDependency(Logger)
console.log('\n')
logger.startSpinner('Hot reloading...')

// Remove events
client.removeEvents()

// cleanup
MetadataStorage.clear()
DIService.engine.clearAllServices()

// transfer store instance to the new container in order to keep the same states
container.registerInstance(Store, store)

// reload files
await loadFiles(importPattern)

// rebuild
await MetadataStorage.instance.build()
await client.initApplicationCommands()
client.initEvents()

// re-init services

// plugins
const pluginManager = await resolveDependency(PluginsManager)
await pluginManager.loadPlugins()
// await pluginManager.execMains() # TODO: need this?

// database
const db = await resolveDependency(Database)
await db.initialize(false)

logger.log(chalk.whiteBright('Hot reloaded'))
}

async function init() {

// init logger, pluginsmanager and error handler
const logger = await resolveDependency(Logger)

// init error handler
await resolveDependency(ErrorHandler)

// init plugins
const pluginManager = await resolveDependency(PluginsManager)

// load plugins and import translations
await pluginManager.loadPlugins()
await pluginManager.syncTranslations()

Expand All @@ -46,12 +106,14 @@ async function run() {
container.registerInstance(Client, client)

// import all the commands and events
await importx(__dirname + "/{events,commands}/**/*.{ts,js}")
await loadFiles(importPattern)
await pluginManager.importCommands()
await pluginManager.importEvents()

RequestContext.create(db.orm.em, async () => {

const watcher = chokidar.watch(importPattern)

// init the data table if it doesn't exist
await initDataTable()

Expand All @@ -66,6 +128,18 @@ async function run() {
client.login(process.env.BOT_TOKEN)
.then(async () => {

if (process.env.NODE_ENV === 'development') {

// reload commands and events when a file changes
watcher.on('change', () => reload(client))

// reload commands and events when a file is added
watcher.on('add', () => reload(client))

// reload commands and events when a file is deleted
watcher.on('unlink', () => reload(client))
}

// start the api server
if (apiConfig.enabled) {
const server = await resolveDependency(Server)
Expand Down Expand Up @@ -108,4 +182,4 @@ async function run() {

}

run()
init()
26 changes: 14 additions & 12 deletions src/services/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ export class Database {

constructor(
@inject(delay(() => Logger)) private logger: Logger
) { }
) {}

async initialize() {
async initialize(migrate = true) {

const pluginsManager = await resolveDependency(PluginsManager)

Expand All @@ -32,17 +32,19 @@ export class Database {
// initialize the ORM using the configuration exported in `mikro-orm.config.ts`
this._orm = await MikroORM.init(config)

const migrator = this._orm.getMigrator()

// create migration if no one is present in the migrations folder
const pendingMigrations = await migrator.getPendingMigrations()
const executedMigrations = await migrator.getExecutedMigrations()
if (pendingMigrations.length === 0 && executedMigrations.length === 0) {
await migrator.createInitialMigration()
if (migrate) {
const migrator = this._orm.getMigrator()

// create migration if no one is present in the migrations folder
const pendingMigrations = await migrator.getPendingMigrations()
const executedMigrations = await migrator.getExecutedMigrations()
if (pendingMigrations.length === 0 && executedMigrations.length === 0) {
await migrator.createInitialMigration()
}

// migrate to the latest migration
await this._orm.getMigrator().up()
}

// migrate to the latest migration
await this._orm.getMigrator().up()
}

async refreshConnection() {
Expand Down
34 changes: 18 additions & 16 deletions src/services/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,13 @@ import { delay, inject, singleton } from "tsyringe"

import * as controllers from "@api/controllers"
import { apiConfig, logsConfig } from "@configs"
import { Pastebin, PluginsManager, Scheduler, WebSocket } from "@services"
import { Pastebin, PluginsManager, Scheduler, Store, WebSocket } from "@services"
import { fileOrDirectoryExists, formatDate, getTypeOfInteraction, numberAlign, oneLine, resolveAction, resolveChannel, resolveDependency, resolveGuild, resolveUser, validString } from "@utils/functions"

const defaultConsole = { ...console }
@singleton()
export class Logger {

constructor(
@inject(delay(() => Client)) private client: Client,
@inject(delay(() => Scheduler)) private scheduler: Scheduler,
@inject(delay(() => WebSocket)) private ws: WebSocket,
@inject(delay(() => Pastebin)) private pastebin: Pastebin,
@inject(delay(() => PluginsManager)) private pluginsManager: PluginsManager
) {
this.defaultConsole = { ...console }
console.info = (...args) => this.log(args.join(", "), 'info')
console.warn = (...args) => this.log(args.join(", "), 'warn')
console.error = (...args) => this.log(args.join(", "), 'error')
}

private readonly logPath: string = `${__dirname}/../../logs`
private readonly levels = ['info', 'warn', 'error'] as const
private embedLevelBuilder = {
Expand All @@ -45,7 +33,21 @@ export class Logger {
"MODAL_SUBMIT_INTERACTION": "Modal submit",
}
private spinner = ora()
private defaultConsole: typeof console

constructor(
@inject(delay(() => Client)) private client: Client,
@inject(delay(() => Scheduler)) private scheduler: Scheduler,
@inject(delay(() => Store)) private store: Store,
@inject(delay(() => WebSocket)) private ws: WebSocket,
@inject(delay(() => Pastebin)) private pastebin: Pastebin,
@inject(delay(() => PluginsManager)) private pluginsManager: PluginsManager
) {
if (!this.store.get('botHasBeenReloaded')) {
console.info = (...args) => this.log(args.join(", "), 'info')
console.warn = (...args) => this.log(args.join(", "), 'warn')
console.error = (...args) => this.log(args.join(", "), 'error')
}
}

// =================================
// ======== Output Providers =======
Expand All @@ -66,7 +68,7 @@ export class Logger {
let templatedMessage = ignoreTemplate ? message : `${level} [${chalk.dim.gray(formatDate(new Date()))}] ${message}`
if (level === 'error') templatedMessage = chalk.red(templatedMessage)

this.defaultConsole[level](templatedMessage)
defaultConsole[level](templatedMessage)

this.websocket(level, message)
}
Expand Down
2 changes: 2 additions & 0 deletions src/services/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { singleton } from "tsyringe"
interface State {

authorizedAPITokens: string[]
botHasBeenReloaded: boolean
ready: {
bot: boolean | null
api: boolean | null
Expand All @@ -15,6 +16,7 @@ interface State {
const initialState: State = {

authorizedAPITokens: [],
botHasBeenReloaded: false,
ready: {
bot: false,
api: apiConfig.enabled ? false : null,
Expand Down

0 comments on commit 69f6887

Please sign in to comment.