Skip to content

Commit 50cc4a0

Browse files
committed
feat: add live reloading
1 parent ef6c7d4 commit 50cc4a0

File tree

6 files changed

+179
-43
lines changed

6 files changed

+179
-43
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323
},
2424
"dependencies": {
2525
"c12": "^1.10.0",
26+
"chokidar": "^3.6.0",
2627
"consola": "^3.2.3",
2728
"discord.js": "^14.15.2",
2829
"dotenv": "^16.4.5",
2930
"globby": "^14.0.1",
3031
"jiti": "^1.21.0",
3132
"pathe": "^1.1.2",
33+
"perfect-debounce": "^1.0.0",
3234
"unctx": "^2.3.1"
3335
},
3436
"devDependencies": {

pnpm-lock.yaml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/harmonix.ts

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import type { LoadConfigOptions } from 'c12'
33
import { getContext } from 'unctx'
44
import consola from 'consola'
55
import { colors } from 'consola/utils'
6+
import { watch } from 'chokidar'
7+
import { resolve } from 'pathe'
8+
import { debounce } from 'perfect-debounce'
69
import { loadOptions } from './options'
710
import {
811
scanButtons,
@@ -43,6 +46,7 @@ import {
4346
} from './resolve'
4447
import type { Harmonix, HarmonixConfig, HarmonixOptions } from './types'
4548
import { version } from '../package.json'
49+
import { Stats } from 'fs'
4650

4751
export const ctx = getContext<Harmonix>('harmonix')
4852

@@ -52,6 +56,11 @@ export const createHarmonix = async (
5256
config: HarmonixConfig = {},
5357
opts: LoadConfigOptions = {}
5458
) => {
59+
if (!process.env.DISCORD_CLIENT_TOKEN) {
60+
createError(
61+
'Client token is required. Please provide it in the environment variable DISCORD_CLIENT_TOKEN.'
62+
)
63+
}
5564
const options = await loadOptions(config, opts)
5665
const harmonix: Harmonix = {
5766
options: options as HarmonixOptions,
@@ -64,6 +73,54 @@ export const createHarmonix = async (
6473
preconditions: new Collection()
6574
}
6675

76+
consola.log(colors.blue(`Harmonix ${colors.bold(version)}\n`))
77+
const watcher = watch(harmonix.options.scanDirs, {
78+
ignored: harmonix.options.ignore,
79+
ignoreInitial: true
80+
})
81+
82+
const reload = debounce(
83+
async (event: string, path: string, stats: Stats | undefined) => {
84+
if (stats?.size === 0) return
85+
consola.info(
86+
`${colors.blue(`lr: ${event}`)}`,
87+
`${colors.gray(resolve(path).replace(harmonix.options.rootDir, ''))}`
88+
)
89+
clearHarmonix(harmonix)
90+
try {
91+
await loadHarmonix(harmonix, config, opts)
92+
} catch (error: any) {
93+
createError(error.message)
94+
}
95+
},
96+
100
97+
)
98+
99+
await loadHarmonix(harmonix, config, opts)
100+
watcher.on('all', (event, path, stats) => reload(event, path, stats))
101+
102+
return harmonix
103+
}
104+
105+
export const clearHarmonix = async (harmonix: Harmonix) => {
106+
harmonix.client?.destroy()
107+
harmonix.events.clear()
108+
harmonix.commands.clear()
109+
harmonix.contextMenus.clear()
110+
harmonix.buttons.clear()
111+
harmonix.modals.clear()
112+
harmonix.selectMenus.clear()
113+
harmonix.preconditions.clear()
114+
}
115+
116+
export const loadHarmonix = async (
117+
harmonix: Harmonix,
118+
config: HarmonixConfig,
119+
opts: LoadConfigOptions
120+
) => {
121+
const options = await loadOptions(config, opts)
122+
123+
harmonix.options = options as HarmonixOptions
67124
const scannedCommands = await scanCommands(harmonix)
68125
const _commands = [...(harmonix.options.commands || []), ...scannedCommands]
69126
const commands = _commands.map((cmd) => resolveCommand(cmd, harmonix.options))
@@ -107,13 +164,6 @@ export const createHarmonix = async (
107164
resolvePrecondition(prc, harmonix.options)
108165
)
109166

110-
if (!process.env.DISCORD_CLIENT_TOKEN) {
111-
createError(
112-
'Client token is required. Please provide it in the environment variable DISCORD_CLIENT_TOKEN.'
113-
)
114-
}
115-
consola.log(colors.blue(`Harmonix ${colors.bold(version)}\n`))
116-
117167
loadEvents(harmonix, events)
118168
loadCommands(harmonix, commands)
119169
loadContextMenus(harmonix, contextMenus)
@@ -132,8 +182,6 @@ export const createHarmonix = async (
132182
registerModals(harmonix)
133183
registerSelectMenus(harmonix)
134184
registerAutocomplete(harmonix)
135-
136-
return harmonix
137185
}
138186

139187
export const createError = (message: string) => {

src/options.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ export const loadOptions = async (
1919
const { config } = await loadConfig<HarmonixConfig>({
2020
name: 'harmonix',
2121
configFile: 'harmonix.config',
22-
rcFile: '.harmonixrc',
2322
dotenv: true,
24-
globalRc: true,
2523
overrides: configOverrides,
2624
defaults: HarmonixDefaults,
2725
...opts

src/resolve.ts

Lines changed: 113 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import jiti from 'jiti'
1+
import createJiti from 'jiti'
22
import { resolve } from 'pathe'
33
import { filename } from 'pathe/utils'
44
import type {
@@ -27,23 +27,33 @@ import {
2727
HarmonixSelectMenuInput,
2828
SelectMenuConfig
2929
} from './types/select-menus'
30+
import consola from 'consola'
3031

3132
export const resolveEvent = (
3233
evt: HarmonixEventInput,
3334
harmonixOptions: Harmonix['options']
3435
): HarmonixEvent => {
3536
if (typeof evt === 'string') {
36-
const _jiti = jiti(harmonixOptions.rootDir, {
37-
interopDefault: true
37+
const jiti = createJiti(harmonixOptions.rootDir, {
38+
cache: false,
39+
interopDefault: true,
40+
requireCache: false,
41+
esmResolve: true
3842
})
39-
const _evtPath = _jiti.resolve(evt)
40-
const event = _jiti(_evtPath) as HarmonixEvent
43+
const _evtPath = jiti.resolve(evt)
44+
const event = jiti(_evtPath) as HarmonixEvent
45+
46+
if (!event.options || !event.callback) {
47+
consola.warn(`Event ${filename(_evtPath)} does not export a valid event.`)
48+
return { options: { name: filename(_evtPath) }, callback: () => {} }
49+
}
4150
const matchSuffix = filename(_evtPath).match(/\.(on|once)?$/)
42-
const type = event.options.type ?? (matchSuffix ? matchSuffix[1] : null)
51+
const once =
52+
event.options.once ?? (matchSuffix ? matchSuffix[1] === 'once' : false)
4353
const options: EventOptions = {
4454
name:
4555
event.options.name || filename(_evtPath).replace(/\.(on|once)?$/, ''),
46-
once: event.options.once || type === 'once'
56+
once: event.options.once || once
4757
}
4858

4959
return { options, callback: event.callback }
@@ -57,11 +67,21 @@ export const resolveCommand = (
5767
harmonixOptions: Harmonix['options']
5868
): HarmonixCommand => {
5969
if (typeof cmd === 'string') {
60-
const _jiti = jiti(harmonixOptions.rootDir, {
61-
interopDefault: true
70+
const jiti = createJiti(harmonixOptions.rootDir, {
71+
cache: false,
72+
interopDefault: true,
73+
requireCache: false,
74+
esmResolve: true
6275
})
63-
const _cmdPath = _jiti.resolve(cmd)
64-
const command = _jiti(_cmdPath) as HarmonixCommand
76+
const _cmdPath = jiti.resolve(cmd)
77+
const command = jiti(_cmdPath) as HarmonixCommand
78+
79+
if (!command.config || !command.execute) {
80+
consola.warn(
81+
`Command ${filename(_cmdPath)} does not export a valid command.`
82+
)
83+
return { config: { name: filename(_cmdPath) }, execute: () => {} }
84+
}
6585
const relativePath = resolve(_cmdPath).replace(harmonixOptions.rootDir, '')
6686
const categoryMatch = relativePath.match(
6787
/\/commands\/(.+?)\/[^\/]+\.(ts|js)/
@@ -84,11 +104,21 @@ export const resolveContextMenu = (
84104
harmonixOptions: Harmonix['options']
85105
): HarmonixContextMenu => {
86106
if (typeof ctm === 'string') {
87-
const _jiti = jiti(harmonixOptions.rootDir, {
88-
interopDefault: true
107+
const jiti = createJiti(harmonixOptions.rootDir, {
108+
cache: false,
109+
interopDefault: true,
110+
requireCache: false,
111+
esmResolve: true
89112
})
90-
const _ctmPath = _jiti.resolve(ctm)
91-
const contextMenu = _jiti(_ctmPath) as HarmonixContextMenu
113+
const _ctmPath = jiti.resolve(ctm)
114+
const contextMenu = jiti(_ctmPath) as HarmonixContextMenu
115+
116+
if (!contextMenu.config || !contextMenu.callback) {
117+
consola.warn(
118+
`Context Menu ${filename(_ctmPath)} does not export a valid context menu.`
119+
)
120+
return { config: { name: filename(_ctmPath) }, callback: () => {} }
121+
}
92122
const matchSuffix = filename(_ctmPath).match(/\.(user|message)?$/)
93123
const type =
94124
contextMenu.config.type ??
@@ -113,11 +143,24 @@ export const resolveButton = (
113143
harmonixOptions: Harmonix['options']
114144
): HarmonixButton => {
115145
if (typeof btn === 'string') {
116-
const _jiti = jiti(harmonixOptions.rootDir, {
117-
interopDefault: true
146+
const jiti = createJiti(harmonixOptions.rootDir, {
147+
cache: false,
148+
interopDefault: true,
149+
requireCache: false,
150+
esmResolve: true
118151
})
119-
const _btnPath = _jiti.resolve(btn)
120-
const button = _jiti(_btnPath) as HarmonixButton
152+
const _btnPath = jiti.resolve(btn)
153+
const button = jiti(_btnPath) as HarmonixButton
154+
155+
if (!button.config || !button.callback) {
156+
consola.warn(
157+
`Button ${filename(_btnPath)} does not export a valid button.`
158+
)
159+
return {
160+
config: { id: filename(_btnPath), label: '' },
161+
callback: () => {}
162+
}
163+
}
121164
const config: ButtonConfig = {
122165
id: button.config.id || filename(_btnPath),
123166
...button.config
@@ -134,11 +177,22 @@ export const resolveModal = (
134177
harmonixOptions: Harmonix['options']
135178
): HarmonixModal => {
136179
if (typeof mdl === 'string') {
137-
const _jiti = jiti(harmonixOptions.rootDir, {
138-
interopDefault: true
180+
const jiti = createJiti(harmonixOptions.rootDir, {
181+
cache: false,
182+
interopDefault: true,
183+
requireCache: false,
184+
esmResolve: true
139185
})
140-
const _mdlPath = _jiti.resolve(mdl)
141-
const modal = _jiti(_mdlPath) as HarmonixModal
186+
const _mdlPath = jiti.resolve(mdl)
187+
const modal = jiti(_mdlPath) as HarmonixModal
188+
189+
if (!modal.config || !modal.callback) {
190+
consola.warn(`Modal ${filename(_mdlPath)} does not export a valid modal.`)
191+
return {
192+
config: { id: filename(_mdlPath), title: '' },
193+
callback: () => {}
194+
}
195+
}
142196
const config: ModalConfig = {
143197
id: modal.config.id || filename(_mdlPath),
144198
...modal.config
@@ -155,11 +209,29 @@ export const resolveSelectMenu = (
155209
harmonixOptions: Harmonix['options']
156210
): HarmonixSelectMenu => {
157211
if (typeof slm === 'string') {
158-
const _jiti = jiti(harmonixOptions.rootDir, {
159-
interopDefault: true
212+
const jiti = createJiti(harmonixOptions.rootDir, {
213+
cache: false,
214+
interopDefault: true,
215+
requireCache: false,
216+
esmResolve: true
160217
})
161-
const _slmPath = _jiti.resolve(slm)
162-
const selectMenu = _jiti(_slmPath) as HarmonixSelectMenu
218+
const _slmPath = jiti.resolve(slm)
219+
const selectMenu = jiti(_slmPath) as HarmonixSelectMenu
220+
221+
if (!selectMenu.config || !selectMenu.callback) {
222+
consola.warn(
223+
`Select Menu ${filename(_slmPath)} does not export a valid select menu.`
224+
)
225+
return {
226+
config: {
227+
id: filename(_slmPath),
228+
placeholder: '',
229+
type: 'String',
230+
options: []
231+
},
232+
callback: () => {}
233+
}
234+
}
163235
const config: SelectMenuConfig = {
164236
id: selectMenu.config.id || filename(_slmPath),
165237
...selectMenu.config
@@ -176,11 +248,21 @@ export const resolvePrecondition = (
176248
harmonixOptions: Harmonix['options']
177249
) => {
178250
if (typeof prc === 'string') {
179-
const _jiti = jiti(harmonixOptions.rootDir, {
180-
interopDefault: true
251+
const jiti = createJiti(harmonixOptions.rootDir, {
252+
cache: false,
253+
interopDefault: true,
254+
requireCache: false,
255+
esmResolve: true
181256
})
182-
const _prcPath = _jiti.resolve(prc)
183-
const precondition = _jiti(_prcPath) as HarmonixPrecondition
257+
const _prcPath = jiti.resolve(prc)
258+
const precondition = jiti(_prcPath) as HarmonixPrecondition
259+
260+
if (!precondition.callback) {
261+
consola.warn(
262+
`Precondition ${filename(_prcPath)} does not export a valid precondition.`
263+
)
264+
return { name: filename(_prcPath), callback: () => {} }
265+
}
184266
const name = precondition.name || filename(_prcPath)
185267

186268
return { name, callback: precondition.callback }

src/types/preconditions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type ContextMenuEntity = {
1818

1919
export type PreconditionCallback = (
2020
entity: SlashEntity | ContextMenuEntity
21-
) => boolean
21+
) => boolean | void
2222

2323
export type DefinePrecondition = (
2424
callback: PreconditionCallback

0 commit comments

Comments
 (0)