Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

131 feature hmr #138

Merged
merged 1 commit into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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