Skip to content

Commit c2843dc

Browse files
committed
feat: server HMR dynamic command loading
1 parent 53e63ba commit c2843dc

File tree

4 files changed

+72
-30
lines changed

4 files changed

+72
-30
lines changed

src/runtime/server/plugins/discord.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import type { ClientOptions } from 'discord.js'
22
import type { WatchEvent } from 'nuxt/schema'
33
import type { SlashCommand, SlashCommandRuntime } from '~/src/types'
4-
// ok this is unreliable but its the best I can do for now...
4+
import { existsSync } from 'node:fs'
55
import slashCommands from 'discord/slashCommands'
66
import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime'
7+
import { logger } from '../internal/logger'
78
import { DiscordClient } from '../utils/client'
89

910
export default defineNitroPlugin(async (nitro) => {
1011
const runtimeConfig = useRuntimeConfig()
1112

1213
const client = new DiscordClient()
13-
1414
;(globalThis as any)[Symbol.for('discord-client')] = client
1515

1616
if ('wsUrl' in (runtimeConfig.public.discord ?? {}) && typeof runtimeConfig.public.discord.wsUrl === 'string') {
@@ -21,21 +21,29 @@ export default defineNitroPlugin(async (nitro) => {
2121
})
2222

2323
socket.addEventListener('message', async (event) => {
24-
// this isn't relevant anymore since we rely on the full nitro server
25-
// to be rebuilt each time and hmr is not really needed, I can't seem
26-
// to figure out how actually make it hot reload without a full rebuild
27-
// TODO: come back if I figure out how to make it work
28-
2924
const data = JSON.parse(event.data) as
3025
| { event: WatchEvent, path: string, command: SlashCommand | null }
3126
| { event: 'full-update', commands: SlashCommand[] }
3227
if (data.event === 'full-update') {
3328
const commands: SlashCommand[] = []
3429
for (const cmd of data.commands) {
3530
const runtimeCommand = cmd as SlashCommandRuntime
36-
// this won't work when command is updated because auto-imports
37-
// in dynamic imports can't be resolved
38-
runtimeCommand.load = () => import(cmd.path).then(m => m.default)
31+
const buildPath = cmd.path
32+
.replace(runtimeConfig.discord.rootDir, runtimeConfig.discord.buildDir)
33+
.replace(/\.ts$/, '.mjs')
34+
const existingCommand = slashCommands.find(c => c.path === cmd.path)
35+
if (existsSync(buildPath)) {
36+
logger.log(`Dynamically loading slash command at ${buildPath}`)
37+
runtimeCommand.load = () => import(buildPath).then(m => m.default)
38+
}
39+
else if (existingCommand) {
40+
runtimeCommand.execute = existingCommand.execute
41+
}
42+
else {
43+
logger.error(`Unable to dynamically load slash command at ${cmd.path}`)
44+
continue
45+
}
46+
3947
commands.push(runtimeCommand)
4048
}
4149
client.clearSlashCommands()
@@ -48,12 +56,6 @@ export default defineNitroPlugin(async (nitro) => {
4856
}
4957
})
5058

51-
socket.addEventListener('open', () => {
52-
// request a full update immediately, since serverTemplate
53-
// updates don't seem to be reliable
54-
socket.send(JSON.stringify({ event: 'full-update' }))
55-
})
56-
5759
nitro.hooks.hook('close', () => socket.close())
5860
}
5961

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export interface DiscordRuntimeConfig {
6565
sync: NuxtDiscordOptions['watch']['sync']
6666
dir: string
6767
buildDir: string
68+
rootDir: string
6869
}
6970

7071
export const slashCommandOptionTypeIdentifiers = [

src/utils/hmr.ts

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import type { Listener } from 'listhen'
22
import type { Buffer } from 'node:buffer'
33
import type { IncomingMessage } from 'node:http'
4+
import type { InputOptions, OutputOptions, RollupBuild } from 'rollup'
45
import type { WebSocket } from 'ws'
56
import type { NuxtDiscordContext, SlashCommand } from '../types'
6-
import { existsSync, globSync, readFileSync, writeFileSync } from 'node:fs'
7+
import { existsSync, globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
8+
import path from 'node:path'
79
import { addVitePlugin, isIgnored, updateTemplates } from '@nuxt/kit'
10+
import chokidar from 'chokidar'
811
import { listen } from 'listhen'
12+
import { rollup } from 'rollup'
913
import { WebSocketServer } from 'ws'
1014
import collectSlashCommands, { processCommandFile } from './collect'
1115

16+
type RollupConfig = InputOptions & {
17+
output: OutputOptions
18+
}
19+
1220
export function prepareHMR(ctx: NuxtDiscordContext) {
1321
const { nuxt, options } = ctx
1422

@@ -23,6 +31,8 @@ export function prepareHMR(ctx: NuxtDiscordContext) {
2331
}
2432

2533
// nitro dev server ignore discord commands directory
34+
let rollupConfig: RollupConfig
35+
2636
nuxt.hook('nitro:config', (nitroConfig) => {
2737
nitroConfig.watchOptions ??= {}
2838
const existingNitroIgnored = nitroConfig.watchOptions.ignored
@@ -34,7 +44,13 @@ export function prepareHMR(ctx: NuxtDiscordContext) {
3444
nitroIgnored.push(existingNitroIgnored)
3545
}
3646
// TODO: stop ignoring the commands directory, come back if I figure out how to make HMR work on server
37-
// nitroConfig.watchOptions.ignored = nitroIgnored
47+
nitroConfig.watchOptions.ignored = nitroIgnored
48+
49+
nitroConfig.hooks ??= {
50+
'rollup:before': (_, config) => {
51+
rollupConfig = config
52+
},
53+
}
3854
})
3955

4056
// vite dev server ignore discord commands directory
@@ -84,19 +100,23 @@ export function prepareHMR(ctx: NuxtDiscordContext) {
84100
})
85101
}
86102

87-
nuxt.hook('builder:watch', async (event, path) => {
88-
const fullPath = ctx.resolve.root(path)
89-
if (fullPath.startsWith(commandsDir)) {
90-
path = ctx.resolve.root(path)
91-
const command = processCommandFile(ctx, path) ?? null
92-
websocket?.broadcast({ event, path, command })
103+
chokidar.watch(ctx.resolve.root(ctx.options.dir, 'commands'), { ignoreInitial: true })
104+
.on('all', async (event, path) => {
105+
const fullPath = ctx.resolve.root(path)
106+
if (fullPath.startsWith(commandsDir)) {
107+
path = ctx.resolve.root(path)
108+
const command = processCommandFile(ctx, path) ?? null
109+
websocket?.broadcast({ event, path, command })
110+
if (event === 'add' || event === 'change') {
111+
await generateDynamicCommandBuild(path, rollupConfig, ctx)
112+
}
93113

94-
// I don't think this is doing anything at all...
95-
await updateTemplates({
96-
filter: template => template.filename === 'discord/slashCommands',
97-
})
98-
}
99-
})
114+
// I don't think this is doing anything at all...
115+
await updateTemplates({
116+
filter: template => template.filename === 'discord/slashCommands',
117+
})
118+
}
119+
})
100120

101121
nuxt.hook('close', () => {
102122
listener?.server.close()
@@ -203,3 +223,21 @@ export function createWebSocket() {
203223
},
204224
}
205225
}
226+
227+
async function generateDynamicCommandBuild(file: string, config: RollupConfig, ctx: NuxtDiscordContext) {
228+
let bundle: RollupBuild
229+
try {
230+
bundle = await rollup({ ...config, input: file })
231+
const { output } = await bundle.generate({ ...config.output, sourcemap: false })
232+
mkdirSync(path.join(ctx.nuxt.options.buildDir, 'discord', 'commands'), { recursive: true })
233+
writeFileSync(file
234+
.replace(ctx.resolve.root(ctx.nuxt.options.rootDir), ctx.nuxt.options.buildDir)
235+
.replace('.ts', '.mjs'), output[0].code, 'utf-8')
236+
}
237+
catch (error) {
238+
ctx.logger.error(`Error processing command file ${file}:`, error)
239+
return
240+
}
241+
242+
await bundle.close()
243+
}

src/utils/runtimeConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ export function prepareRuntimeConfig(ctx: NuxtDiscordContext) {
99
config.runtimeConfig.discord.sync = ctx.options.watch.sync ?? false
1010
config.runtimeConfig.discord.dir = ctx.resolve.root(ctx.options.dir)
1111
config.runtimeConfig.discord.buildDir = ctx.nuxt.options.buildDir
12+
config.runtimeConfig.discord.rootDir = ctx.nuxt.options.rootDir
1213
})
1314
}

0 commit comments

Comments
 (0)