@wolfstar/http-framework 6.1.0 → 6.1.1-next-20261004142621

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,686 +1,700 @@
1
- <div align="center">
2
- <picture>
3
- <img src="https://cdn.wolfstar.rocks/assets/stars-components/wordmark.webp" alt="Stars Components" width="440" />
4
- </picture>
5
-
6
- # @wolfstar/http-framework
7
-
8
- **The HTTP-only Discord bot framework powering the Star Network.**
9
-
10
- [![version](https://npmx.dev/api/registry/badge/version/@wolfstar/http-framework)](https://npmx.dev/package/@wolfstar/http-framework)
11
- [![downloads](https://npmx.dev/api/registry/badge/downloads/@wolfstar/http-framework)](https://npmx.dev/package/@wolfstar/http-framework)
12
- [![license](https://img.shields.io/github/license/wolfstar-project/stars-components?style=flat-square&color=informational)](https://github.com/wolfstar-project/stars-components/blob/main/LICENSE)
13
-
14
- </div>
15
-
16
- ## Description
17
-
18
- A powerful HTTP framework for building your Discord bots, powered by [`node:http`], [`@discordjs/rest`], and [`@sapphire/pieces`].
19
-
20
- ## Features
21
-
22
- - Support for reloading and unloading commands
23
- - Built-in Hot Module Reloading for every store
24
- - Built-in logger, extendable by plugins
25
- - Support for attachment responses
26
- - Seamless integration with low-level libraries
27
- - Thin wrapper on top of raw data for maximum performance
28
-
29
- ## Usage
30
-
31
- This library can handle both HTTP interactions and registering commands both globally and per guild using an integrated design powered by decorators.
32
-
33
- ### Command
34
-
35
- The Command is a piece that runs for all chat input and context menu interactions, including auto-complete (since this one is sort of part of the former). Registering the commands happens with decorators:
36
-
37
- ```typescript
38
- import { Command, RegisterCommand } from '@wolfstar/http-framework';
39
-
40
- @RegisterCommand((builder) =>
41
- builder //
42
- .setName('ping')
43
- .setDescription('Runs a network connection test with me')
44
- )
45
- export class UserCommand extends Command {
46
- public override chatInputRun(interaction: Command.ChatInputInteraction) {
47
- return interaction.sendMessage({ content: 'Pong!' });
48
- }
49
- }
50
- ```
51
-
52
- You can also register subcommands via decorators:
53
-
54
- ```typescript
55
- import { Command, RegisterCommand, RegisterSubcommand } from '@wolfstar/http-framework';
56
-
57
- @RegisterCommand((builder) =>
58
- builder //
59
- .setName('math')
60
- .setDescription('Does some maths.')
61
- )
62
- export class UserCommand extends Command {
63
- @RegisterSubcommand(buildSubcommandBuilders('add', 'Adds the first number to the second number'))
64
- public add(interaction: Command.ChatInputInteraction, { first, second }: Args) {
65
- return interaction.sendMessage({
66
- content: `The result is: ${first + second}`
67
- });
68
- }
69
-
70
- @RegisterSubcommand(buildSubcommandBuilders('subtract', 'Subtracts the second number from the first number'))
71
- public subtract(interaction: Command.ChatInputInteraction, { first, second }: Args) {
72
- return interaction.sendMessage({
73
- content: `The result is: ${first - second}`
74
- });
75
- }
76
- }
77
-
78
- function buildSubcommandBuilders(name: string, description: string) {
79
- return new SlashCommandSubcommandBuilder() //
80
- .setName(name)
81
- .setDescription(description)
82
- .addNumberOption((builder) =>
83
- builder //
84
- .setName('first')
85
- .setDescription('The first number.')
86
- .setRequired(true)
87
- )
88
- .addNumberOption((builder) =>
89
- builder //
90
- .setName('second')
91
- .setDescription('The second number.')
92
- .setRequired(true)
93
- );
94
- }
95
-
96
- interface Args {
97
- first: number;
98
- second: number;
99
- }
100
- ```
101
-
102
- ### Registering commands without decorators
103
-
104
- If you don't want to rely on TS decorators (for example, when writing plain JavaScript, or [`@sapphire/framework`]-style
105
- codebases), you can instead override the `registerApplicationCommands` method, which receives a per-command registry
106
- with the same capabilities as the decorators above:
107
-
108
- ```typescript
109
- import { Command } from '@wolfstar/http-framework';
110
-
111
- export class UserCommand extends Command {
112
- registerApplicationCommands(registry) {
113
- registry.registerChatInputCommand((builder) =>
114
- builder //
115
- .setName('ping')
116
- .setDescription('Runs a network connection test with me')
117
- );
118
- }
119
-
120
- chatInputRun(interaction) {
121
- return interaction.sendMessage({ content: 'Pong!' });
122
- }
123
- }
124
- ```
125
-
126
- Subcommands, subcommand groups, context menu commands, and guild restriction are all available on the registry:
127
-
128
- ```typescript
129
- import { Command } from '@wolfstar/http-framework';
130
-
131
- export class UserCommand extends Command {
132
- registerApplicationCommands(registry) {
133
- registry
134
- .registerChatInputCommand((builder) => builder.setName('math').setDescription('Does some maths.'))
135
- .registerSubcommand((builder) => buildSubcommandBuilders(builder, 'add', 'Adds the first number to the second number'), 'add')
136
- .registerSubcommand(
137
- (builder) => buildSubcommandBuilders(builder, 'subtract', 'Subtracts the second number from the first number'),
138
- 'subtract'
139
- );
140
- }
141
-
142
- add(interaction, { first, second }) {
143
- return interaction.sendMessage({ content: `The result is: ${first + second}` });
144
- }
145
-
146
- subtract(interaction, { first, second }) {
147
- return interaction.sendMessage({ content: `The result is: ${first - second}` });
148
- }
149
- }
150
- ```
151
-
152
- > **Note**: this is an alternative to the decorators, not a replacement — both approaches share the same underlying
153
- > registry and can be mixed across different commands in the same project.
154
-
155
- ### Utility decorators
156
-
157
- The utility decorators for configuring pieces and gating methods — `ApplyOptions`, `RequiresGuildContext`,
158
- `RequiresDMContext`, `RequiresUserPermissions`, `RequiresClientPermissions`, `Enumerable`, `EnumerableMethod`, and the
159
- `createClassDecorator`/`createMethodDecorator`/`createProxy`/`createFunctionPrecondition` primitives — live in their own
160
- package, [`@wolfstar/decorators`](https://npmx.dev/package/@wolfstar/decorators), the same way `@sapphire/framework`
161
- pairs with `@sapphire/decorators`:
162
-
163
- ```typescript
164
- import { ApplyOptions, RequiresGuildContext } from '@wolfstar/decorators';
165
- import { Command, RegisterCommand } from '@wolfstar/http-framework';
166
-
167
- @ApplyOptions<Command.Options>({ name: 'kick' })
168
- @RegisterCommand({ name: 'kick', description: 'Kicks a member' })
169
- export class UserCommand extends Command {
170
- @RequiresGuildContext()
171
- public override chatInputRun(interaction: Command.ChatInputInteraction) {
172
- return interaction.reply({ content: `Hello from ${interaction.guildId}!` });
173
- }
174
- }
175
- ```
176
-
177
- ### Client
178
-
179
- The `Client` class contains the HTTP server, powered by [`node:http`], it also registers a handler that processes whether or not the HTTP request comes from Discord and processes the information accordingly, handling the heavyweight in the background.
180
-
181
- ```typescript
182
- import { Client } from '@wolfstar/http-framework';
183
-
184
- const client = new Client({
185
- discordToken: process.env.DISCORD_TOKEN,
186
- discordPublicKey: process.env.DISCORD_PUBLIC_KEY
187
- });
188
-
189
- // Load all the commands and message component handlers:
190
- await client.load();
191
-
192
- // Start up the HTTP server;
193
- await client.listen({ port: 3000 });
194
- ```
195
-
196
- `Client#fetch` is the Web `Request`/`Response` counterpart of `listen()` — the same signature verification, routing
197
- and replies, without binding a port. Use it to serve interactions from anything that speaks Fetch instead of
198
- `node:http` (Nitro, a Worker, `Bun.serve`, `Deno.serve`, Vite's own dev middleware):
199
-
200
- ```typescript
201
- export default {
202
- fetch: (request: Request) => client.fetch(request, { postPath: '/interactions' })
203
- };
204
- ```
205
-
206
- This is what `@wolfstar/cli`'s `experimental.enableNitro` uses under the hood: the generated Nitro server entry calls
207
- `client.fetch(request)` directly instead of `listen()`, so `node:http` is swapped for whatever preset Nitro targets.
208
-
209
- ### Logger
210
-
211
- The framework ships a minimal logger, available as `container.logger` (and as `client.logger`) as soon as
212
- `@wolfstar/http-framework` is imported. It writes to the matching `console` method and filters entries by
213
- `LogLevel`, which defaults to `LogLevel.Info`:
214
-
215
- ```typescript
216
- import { container, Client, LogLevel } from '@wolfstar/http-framework';
217
-
218
- const client = new Client({ logger: { level: LogLevel.Debug } });
219
-
220
- container.logger.info('Ready');
221
- container.logger.debug('Interaction received', interaction.id);
222
- ```
223
-
224
- The built-in implementation is intentionally bare: it has no timestamps, colours, or transports. Those belong to a
225
- logger plugin, which replaces it by assigning an `ILogger` to `options.logger.instance` from a
226
- `preGenericsInitialization` hook:
227
-
228
- ```typescript
229
- import { Plugin, preGenericsInitialization, type ClientOptions } from '@wolfstar/http-framework';
230
-
231
- export class LoggerPlugin extends Plugin {
232
- public static [preGenericsInitialization](options: ClientOptions): void {
233
- options.logger ??= {};
234
- options.logger.instance = new MyLogger(options.logger);
235
- }
236
- }
237
- ```
238
-
239
- Because the plugin only has to satisfy the `ILogger` interface, the rest of the framework — including Hot Module
240
- Reloading and the command router — keeps logging through `container.logger` without any change.
241
-
242
- ### Client events
243
-
244
- The `Client` extends an event emitter typed by the `ClientEvents` interface. Every event name is also available as a
245
- member of the `Events` enum, which is the recommended way to reference them, as the plain strings remain valid:
246
-
247
- ```typescript
248
- import { Events } from '@wolfstar/http-framework';
249
-
250
- client.on(Events.CommandError, (error, context) => {
251
- console.error(`Failed to run ${context.command.name}`, error);
252
- });
253
- ```
254
-
255
- | Enum member | Event name | Arguments |
256
- | -------------------------------------- | ------------------------------- | --------------------------------------------- |
257
- | `Events.Error` | `error` | `error: unknown` |
258
- | `Events.PluginLoaded` | `pluginLoaded` | `hook: PluginHook, name: string \| undefined` |
259
- | `Events.CommandNameMissing` | `commandNameMissing` | `interaction, response` |
260
- | `Events.CommandNameUnknown` | `commandNameUnknown` | `interaction, response` |
261
- | `Events.CommandMethodUnknown` | `commandMethodUnknown` | `context` |
262
- | `Events.CommandRun` | `commandRun` | `context` |
263
- | `Events.CommandSuccess` | `commandSuccess` | `context, value: unknown` |
264
- | `Events.CommandError` | `commandError` | `error: unknown, context` |
265
- | `Events.CommandFinish` | `commandFinish` | `context` |
266
- | `Events.AutocompleteRun` | `autocompleteRun` | `context` |
267
- | `Events.AutocompleteSuccess` | `autocompleteSuccess` | `context, value: unknown` |
268
- | `Events.AutocompleteError` | `autocompleteError` | `error: unknown, context` |
269
- | `Events.AutocompleteFinish` | `autocompleteFinish` | `context` |
270
- | `Events.InteractionHandlerNameInvalid` | `interactionHandlerNameInvalid` | `interaction, response` |
271
- | `Events.InteractionHandlerNameUnknown` | `interactionHandlerNameUnknown` | `interaction, response` |
272
- | `Events.InteractionHandlerRun` | `interactionHandlerRun` | `context` |
273
- | `Events.InteractionHandlerSuccess` | `interactionHandlerSuccess` | `context, value: unknown` |
274
- | `Events.InteractionHandlerError` | `interactionHandlerError` | `error: unknown, context` |
275
- | `Events.InteractionHandlerFinish` | `interactionHandlerFinish` | `context` |
276
-
277
- The Hot Module Reloading events are listed in [their own section](#hot-module-reloading).
278
-
279
- Listeners declared as pieces can use the enum too:
280
-
281
- ```typescript
282
- import { ApplyOptions } from '@wolfstar/decorators';
283
- import { Events, Listener } from '@wolfstar/http-framework';
284
-
285
- @ApplyOptions<Listener.Options>({ event: Events.CommandError })
286
- export class UserListener extends Listener {
287
- public run(error: unknown, context: ClientEventCommandContext) {
288
- console.error(`Failed to run ${context.command.name}`, error);
289
- }
290
- }
291
- ```
292
-
293
- ### Hot Module Reloading
294
-
295
- `@wolfstar/http-framework` ships with Hot Module Reloading (HMR) as a core feature, no plugin required. When enabled,
296
- every path registered in every store is watched, and pieces are loaded, reloaded, and unloaded in place as their files
297
- are created, changed, and deleted, without restarting the process.
298
-
299
- ```typescript
300
- import { Client } from '@wolfstar/http-framework';
301
-
302
- const client = new Client({
303
- discordToken: process.env.DISCORD_TOKEN,
304
- discordPublicKey: process.env.DISCORD_PUBLIC_KEY,
305
- // Development only, do not enable this in production:
306
- hmr: { enabled: process.env.NODE_ENV !== 'production' }
307
- });
308
-
309
- // `load` starts the reloader once the stores have been loaded:
310
- await client.load();
311
- ```
312
-
313
- The `hmr` option accepts [all of chokidar's options], plus:
314
-
315
- | Option | Default | Description |
316
- | --------- | ------- | ------------------------------------------------------------------------------------------ |
317
- | `enabled` | `true` | Whether HMR is started. Omitting the `hmr` option entirely also leaves the reloader unset. |
318
- | `silent` | `false` | Whether the reloader refrains from writing to the console. Events are always emitted. |
319
-
320
- The reloader is exposed as `client.hmr`, which is `null` when HMR is disabled, and can be stopped at any time:
321
-
322
- ```typescript
323
- await client.hmr?.stop();
324
- ```
325
-
326
- It can also be used standalone, without a `Client`, as long as the stores are registered in the container:
327
-
328
- ```typescript
329
- import { HotModuleReloader } from '@wolfstar/http-framework';
330
-
331
- const reloader = await new HotModuleReloader({ silent: true }).start();
332
- ```
333
-
334
- The client emits an event for every operation, which is useful to react to changes, for example to push the updated
335
- application commands to Discord while developing:
336
-
337
- ```typescript
338
- client.on(Events.HmrPieceReloaded, async (piece) => {
339
- if (piece.store.name === 'commands') {
340
- await client.registry.pushGlobalCommandsInGuild(process.env.DEVELOPMENT_GUILD_ID);
341
- }
342
- });
343
- ```
344
-
345
- | Enum member | Event | Arguments | Description |
346
- | ------------------------- | ------------------ | ----------------------- | ---------------------------------------------------- |
347
- | `Events.HmrStart` | `hmrStart` | `paths: string[]` | The reloader started watching the given store paths. |
348
- | `Events.HmrStop` | `hmrStop` | — | The reloader stopped and closed all of its watchers. |
349
- | `Events.HmrPiecesLoaded` | `hmrPiecesLoaded` | `pieces: Piece[], path` | A new file was created and its pieces were loaded. |
350
- | `Events.HmrPieceReloaded` | `hmrPieceReloaded` | `piece: Piece, path` | An existing file changed and its piece was reloaded. |
351
- | `Events.HmrPieceUnloaded` | `hmrPieceUnloaded` | `piece: Piece, path` | A file was deleted and its piece was unloaded. |
352
- | `Events.HmrError` | `hmrError` | `error: unknown, path` | An operation failed; saving the file again retries. |
353
-
354
- > **Note**: unloading a command also removes its entry from the `ApplicationCommandRegistry`, so a reloaded command is
355
- > registered exactly once. HMR does not push the updated commands to Discord on its own, subscribe to the events above
356
- > if you want that behaviour.
357
-
358
- ### Project configuration (`stars.config.*`)
359
-
360
- The typed project configuration consumed by the [`stars` CLI](../cli) — the `defineConfig` helper and the config
361
- loader — lives in [`@wolfstar/schema`](../schema), a small package shared by `@wolfstar/http-framework`
362
- and `@wolfstar/cli` so neither depends on the other's runtime. `@wolfstar/http-framework` re-exports its public
363
- surface unchanged as `@wolfstar/http-framework/config`, so any tool can resolve a project's configuration without
364
- pulling in `@wolfstar/cli` — most projects should keep importing it from here rather than depending on
365
- `@wolfstar/schema` directly.
366
-
367
- The package also exposes `@wolfstar/http-framework/schema`, forwarding the shared schema's types and loader,
368
- like Nuxt's `nuxt/schema`. Both facades preserve the same exports and function identities; existing `/config`
369
- imports remain supported. Tooling can read the package metadata through `@wolfstar/http-framework/package.json`.
370
-
371
- ```typescript
372
- import type { StarsConfig, ResolvedStarsConfig } from '@wolfstar/http-framework/schema';
373
- ```
374
-
375
- Installing `@wolfstar/http-framework` also gives a project the `stars` binary (it depends on `@wolfstar/cli` and
376
- exposes it as `stars`, the way `nuxt` exposes `nuxi`'s binary), so a project needs no separate `@wolfstar/cli`
377
- install to run `stars dev`/`stars build`.
378
-
379
- ```typescript
380
- // stars.config.ts
381
- import { defineConfig } from '@wolfstar/http-framework/config';
382
-
383
- export default defineConfig({});
384
- ```
385
-
386
- `@wolfstar/http-framework/config` has no side effects — importing it (or a `stars.config.ts` that imports it) never
387
- starts the bot. `loadStarsConfig` discovers `stars.config.{ts,mts,cts,js,mjs,cjs}` from a directory, applies defaults,
388
- validates every option and resolves all paths to absolute ones.
389
-
390
- `dev.url`, the URL `stars dev` shows and health-checks the bot on, needs no configuration either: it is detected the
391
- way Vite's and Nuxt's dev servers are, from `HTTP_PORT` (env var, `src/.env*`/`.env*`, or `dev.env`) or `3000`, and
392
- `localhost` is swapped for `127.0.0.1` at runtime if that is what is actually reachable. Set `dev.url` explicitly only
393
- to override it, e.g. for a LAN address: `dev: { url: 'http://192.168.1.5:3000' }`.
394
-
395
- `dev.banner` replaces the default Stars wordmark in the interactive CLI. Use a string or an array of lines, for
396
- example `dev: { banner: ['★ STARYL', 'Twitch notifications'] }`, or `false` to hide it. Up to four lines fit the
397
- compact panel. Application logs and standalone banners are available behind `l`, not printed over the dev panel.
398
-
399
- `dev` also carries optional overrides for the dev loop. Press `t` in the TUI to toggle a quick tunnel without any
400
- configuration:
401
-
402
- ```typescript
403
- export default defineConfig({
404
- dev: {
405
- // A type checker next to the bot, reported on the dev UI's `tsc` channel. Never blocks a build.
406
- // `checker` is 'tsc' | 'golar' | 'tsz' | 'auto' (default: golar when installed, tsc otherwise).
407
- typecheck: { checker: 'golar' },
408
- // A cloudflared quick tunnel so Discord can reach the interactions endpoint, or an https URL you serve.
409
- tunnel: true,
410
- // Where the session's logs are mirrored, so a run can be read after the terminal UI is gone.
411
- logFile: '.stars/dev.log'
412
- }
413
- });
414
- ```
415
-
416
- `tunnel.updateEndpoint` writes the public URL to the Discord application's `interactions_endpoint_url`; it is opt-in
417
- because it edits a live application, and needs `DISCORD_TOKEN` in the environment or the project's `.env`.
418
-
419
- ### The build (`tsdown`)
420
-
421
- `tsdown` is the bundler `stars build` and `stars dev` use, and it is configured from `stars.config` itself. A base
422
- project configures nothing at all — this is a complete build:
423
-
424
- ```typescript
425
- export default defineConfig({});
426
- ```
427
-
428
- The defaults are the configuration a bot would otherwise write out by hand:
429
-
430
- | Option | Default | Why |
431
- | -------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------- |
432
- | `entry` | every source file next to `entry`, minus `*.test.*`/`*.spec.*` | pieces are found by the stores, not imported by the entry |
433
- | `unbundle` | `true` | keeps `dist/commands/…` loadable one file at a time at runtime |
434
- | `format` / `platform` | `'esm'` / `'node'` | what the framework and `node dist/…` expect |
435
- | `outDir` / `outExtensions` | `build.outDir` / the extension of `build.output` | so `stars dev`, `package.json#main` and `node dist/…` agree |
436
- | `tsconfig` | `build.tsconfig` (`src/tsconfig.json`, else `tsconfig.json`) | `tsdown` alone looks only next to the root, missing the `src/` layout |
437
- | `sourcemap` / `treeshake` | `true` | a deployed bot needs readable stack traces |
438
- | `minify` | `false` | nothing is shipped over a wire, so bytes buy nothing here |
439
- | `deps.neverBundle` | `true` | dependencies stay in `node_modules` instead of being copied into `dist` |
440
- | `alias` | `~`/`@` → the entry's directory, `~~`/`@@` → the project root | the prefixes Nuxt gives every project |
441
- | `dts` | `false` | nothing consumes a bot's `dist/`; set `tsdown: { dts: true }` if yours does |
442
-
443
- #### Aliases
444
-
445
- The four prefixes Nuxt gives every project work out of the box, pointing at the same two places its own do:
446
-
447
- ```typescript
448
- import { Greeting } from '~/lib/greeting'; // and '@/lib/greeting' — the entry's directory
449
- import pkg from '~~/package.json'; // and '@@/package.json' — the project root
450
- ```
451
-
452
- The build resolves them on its own; TypeScript needs the matching `paths` (the scaffold writes them, and
453
- `examples/basic` shows them):
454
-
455
- ```jsonc
456
- {
457
- "compilerOptions": {
458
- "paths": {
459
- "~/*": ["./src/*"],
460
- "@/*": ["./src/*"],
461
- "~~/*": ["./*"],
462
- "@@/*": ["./*"]
463
- }
464
- }
465
- }
466
- ```
467
-
468
- A project's own `tsdown.alias` is added to these rather than replacing them, and a target written as a relative path
469
- (`'./src/lib'`) is resolved against the project root, the way every other path in `stars.config` is — a module id
470
- (`'preact/compat'`) is left alone.
471
-
472
- So the block is for what the defaults cannot know — an extra alias, declaration output, or a target. Conventional
473
- `src/locales` assets are copied to `dist/locales` automatically by the CLI:
474
-
475
- ```typescript
476
- export default defineConfig({
477
- tsdown: { alias: { '#shared': './src/shared' }, dts: true }
478
- });
479
- ```
480
-
481
- Anything in `tsdown` wins over the defaults, and `plugins` are appended rather than replaced.
482
-
483
- With `future.compatibilityVersion: 3` (end-of-life legacy mode) a `tsdown.config.*` in the project root is still
484
- loaded and `tsdown` is merged over it, so a project can move its options across one at a time. From `4` on the block is
485
- the whole configuration, and a leftover `tsdown.config.*` is reported instead of being silently ignored.
486
-
487
- `vite: {}` works the same way for `build.tool: 'vite'` (see [experimental flags](#experimental-flags)): it is merged
488
- into the project's own `vite.config.*`, the way `vite: {}` in a Nuxt config is.
489
-
490
- ### Compatibility version
491
-
492
- `future.compatibilityVersion` selects the build-default generation. Version 5 is the default and version 4 remains
493
- supported. Version 3 is **end-of-life**: it still works for projects that have a standalone `tsdown.config.*`, but
494
- every command prints a `COMPATIBILITY_VERSION_EOL` warning, and it is removed in the next major.
495
-
496
- ```typescript
497
- export default defineConfig({
498
- future: {
499
- compatibilityVersion: 4
500
- }
501
- });
502
- ```
503
-
504
- | Version | What it changes |
505
- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
506
- | `3` (EOL) | Legacy behaviour: a `tsdown.config.*` drives the build, auto imports off unless asked for |
507
- | `4` | `tsdown` configured from `stars.config` alone, auto imports on and wired in, `'auto'` picks `tsdown` for TypeScript; [`env`](#environment-env) is opt-in |
508
- | `5` (default) | Everything in `4`, plus [`env`](#environment-env) registered automatically when the project depends on `@wolfstar/env-utilities` |
509
-
510
- From version 4 on:
511
-
512
- - **Auto imports are on** with the `tsdown` build tool, and the `autoImports()` plugin is wired into the build by
513
- `stars` itself instead of by the project's own configuration file — the framework's exports and the project's
514
- `src/lib/**`, `src/utils/**` are usable without an `import` statement, the same way Nuxt's own are.
515
- - **`tsdown` is configured from `stars.config` alone.** A `tsdown.config.*` (or a `package.json#tsdown` field) raises
516
- `TSDOWN_CONFIG_FILE_UNSUPPORTED`, because a build that quietly dropped the plugins such a file declares would be
517
- far harder to diagnose than an error naming it.
518
- - **`build.tool: 'auto'` resolves to `tsdown`** for any TypeScript entry, rather than looking for a `tsdown.config.*`
519
- or a `tsdown` dependency first. `tsc` stays available as an explicit choice.
520
-
521
- Version 5 adds one thing: **the environment is registered for you.** `stars` calls `setup()` from
522
- `@wolfstar/env-utilities` before any module of the bot runs, so the bot no longer loads its `.env*` files by hand.
523
-
524
- `stars info` prints the version in effect, and any warning about it.
525
-
526
- ### Environment (`env`)
527
-
528
- `env` mirrors the options of `setup()` from `@wolfstar/env-utilities`. `stars` writes them into the built entry and
529
- calls `setup()` with them as the entry's very first import — before `@wolfstar/plugin-*` registrations and the bot's
530
- own modules, all of which may read `process.env` as soon as they load.
531
-
532
- ```typescript
533
- export default defineConfig({
534
- env: { prefix: 'BOT_', loader: 'varlock' }
535
- });
536
- ```
537
-
538
- | Option | Type | Default |
539
- | ---------- | ----------------------- | ------------------------- |
540
- | `enabled` | `boolean` | see below |
541
- | `path` | `string` | `src/.env*`, then `.env*` |
542
- | `env` | `string` | `NODE_ENV` |
543
- | `prefix` | `string` | none |
544
- | `loader` | `'dotenv' \| 'varlock'` | `'dotenv'` |
545
- | `debug` | `boolean` | `false` |
546
- | `encoding` | `string` | `'utf8'` |
547
-
548
- - `env: false` turns the registration off; `env: true` turns it on with the defaults. Any option at all is an opt-in
549
- as well, which requires `@wolfstar/env-utilities` in the project's dependencies (`ENV_REQUIRES_ENV_UTILITIES`).
550
- - Without an `env` block it is on from compatibility version 5, when the project depends on
551
- `@wolfstar/env-utilities`, and off with `experimental.enableNitro` — serverless presets have no `.env` files to load.
552
- - Only the serializable options are mirrored: they end up in the build output as JSON. `path` stays relative and is
553
- resolved against the bot's working directory at runtime, so the output does not depend on the machine that built it.
554
- - tsdown, Vite and Nitro builds register it through the entry transform. `build.tool: 'tsc'` and `'none'` never pass
555
- through it: `stars dev` preloads it with `node --import`, but in production such a bot loads its environment itself
556
- (or runs with `node --import @wolfstar/env-utilities/setup`). The projects `@wolfstar/create-http-framework`
557
- generates for them set `env: false` and keep calling `setup()` in `src/lib/setup`.
558
- - `stars dev` reads `HTTP_PORT` for the default `dev.url` from the same `path`/`env` files, after `env:options`. With
559
- `loader: 'varlock'` it reads it, and the tunnel's Discord credentials, from `varlock load` in the project instead —
560
- run the way the bot loads it, without `env.env`, which varlock ignores.
561
-
562
- ### Hooks
563
-
564
- `hooks` registers lifecycle hooks of the `stars` CLI, run with [`hookable`](https://github.com/unjs/hookable) the way
565
- Nuxt's own `hooks` are. Each hook is keyed by its full name or nested under its namespace, as a function or an array
566
- of functions:
567
-
568
- ```typescript
569
- export default defineConfig({
570
- hooks: {
571
- 'env:options'(options) {
572
- if (process.env.CI) options.path = '.env.ci';
573
- },
574
- build: {
575
- done(outcome) {
576
- if (!outcome.ok) console.error(outcome.message);
577
- }
578
- }
579
- }
580
- });
581
- ```
582
-
583
- | Hook | Arguments | Runs |
584
- | ----------------- | ------------------- | --------------------------------------------------------------------- |
585
- | `config:resolved` | `config` | after `stars.config` is loaded and validated, in every command |
586
- | `env:options` | `options`, `config` | before `env` is written into the build; mutate `options` to change it |
587
- | `prepare:before` | `config` | before `.stars/` is generated |
588
- | `prepare:done` | `config`, `result` | after `.stars/` is generated |
589
- | `builder:created` | `builder`, `config` | once the builder for `build.tool` exists, before it builds |
590
- | `tsdown:options` | `options`, `config` | before `tsdown.build()`; mutate `options` to change the build |
591
- | `build:before` | `config` | before each build (every rebuild in `stars dev`) |
592
- | `build:done` | `outcome`, `config` | after each build, successful or not (`outcome.ok`) |
593
- | `dev:start` | `config` | once `stars dev` is watching |
594
- | `dev:restart` | `reason`, `config` | before `stars dev` (re)starts the bot |
595
- | `dev:close` | `config` | when `stars dev` shuts down |
596
-
597
- Hooks run in the CLI process, never in the bot — `env:options` is how a hook changes what the bot receives. An unknown
598
- hook name fails with `UNKNOWN_HOOK` rather than registering a hook that never runs. In `stars dev` a hook that throws
599
- is logged and the watcher keeps going; in `stars build` it fails the build.
600
-
601
- `stars build` and `stars dev` run them in the same order: `config:resolved` → `env:options` → `builder:created` →
602
- `prepare:before` → `prepare:done`, then `build:before` → `build:done` for each build. `stars dev` adds `dev:start` once
603
- it is watching, `dev:restart` before each (re)start of the bot, and `dev:close` on shutdown. `stars build` awaits every
604
- hook. In `stars dev` the builder drives rebuilds on its own: `build:before` starts when it reports a build, without
605
- delaying it, while `build:done`, `dev:restart` and `dev:close` run one after another — a slow `build:done` settles before
606
- the restart it triggered, and before shutdown. `build:done` gets the final outcome, including a failure to copy
607
- `src/locales`.
608
-
609
- ### Experimental flags
610
-
611
- `experimental` is the same kind of block Nuxt's own `experimental` is: opt-in booleans, all `false` by default, each
612
- guarding work that is still landing.
613
-
614
- ```typescript
615
- export default defineConfig({
616
- entry: 'src/main.ts',
617
- // `build.tool: 'vite'` is only accepted with `enableVite`, and `'auto'` only then detects a vite.config.*
618
- build: { tool: 'vite' },
619
- experimental: {
620
- // Vite as the build tool and the HTTP server, in place of tsdown plus the framework's node:http listener.
621
- enableVite: true,
622
- // The project runs Vite itself: `stars dev` only watches the output and restarts the bot.
623
- enableExternalVite: false,
624
- // Build and serve through Nitro (itself a Vite plugin) instead of node:http, deployable to any of its
625
- // presets; the entry's default export must be the `Client` instance, served through `client.fetch(request)`.
626
- enableNitro: false
627
- }
628
- });
629
- ```
630
-
631
- The resolved configuration is also available programmatically:
632
-
633
- ```typescript
634
- import { loadStarsConfig } from '@wolfstar/http-framework/config';
635
-
636
- const config = await loadStarsConfig({ cwd: process.cwd() });
637
- console.log(config.entry, config.build.output);
638
- ```
639
-
640
- Invalid options raise a `ConfigError` with a stable `code`, the offending option `path`, the `file` it came from, and
641
- an actionable `hint`. See the [`@wolfstar/cli` README](../cli#configuration) for the full option reference and how the
642
- `stars` commands (`dev`, `build`, `info`, `codegen`, `prepare`, `commands`) use it.
643
-
644
- ### ApplicationCommandRegistry
645
-
646
- The `ApplicationCommandRegistry` is `@wolfstar/http-framework`'s centralized registry and uses [`@discordjs/rest`] to register them in Discord.
647
-
648
- ```typescript
649
- // Assuming you have the code above, and that you called `client.load()`:
650
-
651
- // Register all global commands:
652
- await client.registry.pushGlobalCommands();
653
-
654
- // Register all the guild-restricted commands:
655
- await client.registry.pushGuildRestrictedCommands();
656
- ```
657
-
658
- However, if you want to use the registry without the client, you can do so:
659
-
660
- ```typescript
661
- import { applicationCommandRegistry } from '@wolfstar/http-framework';
662
- import { REST } from '@discordjs/rest';
663
-
664
- applicationCommandRegistry.setup({
665
- rest: new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN),
666
- clientId: process.env.DISCORD_CLIENT_ID
667
- });
668
-
669
- // Load all the commands:
670
- await applicationCommandRegistry.loadCommands();
671
-
672
- // Register all global commands:
673
- await applicationCommandRegistry.pushGlobalCommands();
674
-
675
- // Register all the guild-restricted commands:
676
- await applicationCommandRegistry.pushGuildRestrictedCommands();
677
- ```
678
-
679
- > **Note**: calling `applicationCommandRegistry.setup()` is not needed if you are using the `Client` class because it is
680
- > already called automatically for you.
681
-
682
- [all of chokidar's options]: https://github.com/paulmillr/chokidar#api
683
- [`node:http`]: https://nodejs.org/api/http.html
684
- [`@discordjs/rest`]: https://www.npmjs.com/package/@discordjs/rest
685
- [`@sapphire/pieces`]: https://www.npmjs.com/package/@sapphire/pieces
686
- [`@sapphire/framework`]: https://sapphirejs.dev/docs/Guide/commands/application-commands/application-command-registry/
1
+ <div align="center">
2
+ <picture>
3
+ <img src="https://cdn.wolfstar.rocks/assets/stars-components/wordmark.webp" alt="Stars Components" width="440" />
4
+ </picture>
5
+
6
+ # @wolfstar/http-framework
7
+
8
+ **The HTTP-only Discord bot framework powering the Star Network.**
9
+
10
+ [![version](https://npmx.dev/api/registry/badge/version/@wolfstar/http-framework)](https://npmx.dev/package/@wolfstar/http-framework)
11
+ [![downloads](https://npmx.dev/api/registry/badge/downloads/@wolfstar/http-framework)](https://npmx.dev/package/@wolfstar/http-framework)
12
+ [![license](https://img.shields.io/github/license/wolfstar-project/stars-components?style=flat-square&color=informational)](https://github.com/wolfstar-project/stars-components/blob/main/LICENSE)
13
+
14
+ </div>
15
+
16
+ ## Description
17
+
18
+ A powerful HTTP framework for building your Discord bots, powered by [`node:http`], [`@discordjs/rest`], and [`@sapphire/pieces`].
19
+
20
+ ## Features
21
+
22
+ - Support for reloading and unloading commands
23
+ - Built-in Hot Module Reloading for every store
24
+ - Built-in logger, extendable by plugins
25
+ - Support for attachment responses
26
+ - Seamless integration with low-level libraries
27
+ - Thin wrapper on top of raw data for maximum performance
28
+
29
+ ## Usage
30
+
31
+ This library can handle both HTTP interactions and registering commands both globally and per guild using an integrated design powered by decorators.
32
+
33
+ ### Command
34
+
35
+ The Command is a piece that runs for all chat input and context menu interactions, including auto-complete (since this one is sort of part of the former). Registering the commands happens with decorators:
36
+
37
+ ```typescript
38
+ import { Command, RegisterCommand } from '@wolfstar/http-framework';
39
+
40
+ @RegisterCommand((builder) =>
41
+ builder //
42
+ .setName('ping')
43
+ .setDescription('Runs a network connection test with me')
44
+ )
45
+ export class UserCommand extends Command {
46
+ public override chatInputRun(interaction: Command.ChatInputInteraction) {
47
+ return interaction.sendMessage({ content: 'Pong!' });
48
+ }
49
+ }
50
+ ```
51
+
52
+ You can also register subcommands via decorators:
53
+
54
+ ```typescript
55
+ import { Command, RegisterCommand, RegisterSubcommand } from '@wolfstar/http-framework';
56
+
57
+ @RegisterCommand((builder) =>
58
+ builder //
59
+ .setName('math')
60
+ .setDescription('Does some maths.')
61
+ )
62
+ export class UserCommand extends Command {
63
+ @RegisterSubcommand(buildSubcommandBuilders('add', 'Adds the first number to the second number'))
64
+ public add(interaction: Command.ChatInputInteraction, { first, second }: Args) {
65
+ return interaction.sendMessage({
66
+ content: `The result is: ${first + second}`
67
+ });
68
+ }
69
+
70
+ @RegisterSubcommand(buildSubcommandBuilders('subtract', 'Subtracts the second number from the first number'))
71
+ public subtract(interaction: Command.ChatInputInteraction, { first, second }: Args) {
72
+ return interaction.sendMessage({
73
+ content: `The result is: ${first - second}`
74
+ });
75
+ }
76
+ }
77
+
78
+ function buildSubcommandBuilders(name: string, description: string) {
79
+ return new SlashCommandSubcommandBuilder() //
80
+ .setName(name)
81
+ .setDescription(description)
82
+ .addNumberOption((builder) =>
83
+ builder //
84
+ .setName('first')
85
+ .setDescription('The first number.')
86
+ .setRequired(true)
87
+ )
88
+ .addNumberOption((builder) =>
89
+ builder //
90
+ .setName('second')
91
+ .setDescription('The second number.')
92
+ .setRequired(true)
93
+ );
94
+ }
95
+
96
+ interface Args {
97
+ first: number;
98
+ second: number;
99
+ }
100
+ ```
101
+
102
+ ### Registering commands without decorators
103
+
104
+ If you don't want to rely on TS decorators (for example, when writing plain JavaScript, or [`@sapphire/framework`]-style
105
+ codebases), you can instead override the `registerApplicationCommands` method, which receives a per-command registry
106
+ with the same capabilities as the decorators above:
107
+
108
+ ```typescript
109
+ import { Command } from '@wolfstar/http-framework';
110
+
111
+ export class UserCommand extends Command {
112
+ registerApplicationCommands(registry) {
113
+ registry.registerChatInputCommand((builder) =>
114
+ builder //
115
+ .setName('ping')
116
+ .setDescription('Runs a network connection test with me')
117
+ );
118
+ }
119
+
120
+ chatInputRun(interaction) {
121
+ return interaction.sendMessage({ content: 'Pong!' });
122
+ }
123
+ }
124
+ ```
125
+
126
+ Subcommands, subcommand groups, context menu commands, and guild restriction are all available on the registry:
127
+
128
+ ```typescript
129
+ import { Command } from '@wolfstar/http-framework';
130
+
131
+ export class UserCommand extends Command {
132
+ registerApplicationCommands(registry) {
133
+ registry
134
+ .registerChatInputCommand((builder) => builder.setName('math').setDescription('Does some maths.'))
135
+ .registerSubcommand((builder) => buildSubcommandBuilders(builder, 'add', 'Adds the first number to the second number'), 'add')
136
+ .registerSubcommand(
137
+ (builder) => buildSubcommandBuilders(builder, 'subtract', 'Subtracts the second number from the first number'),
138
+ 'subtract'
139
+ );
140
+ }
141
+
142
+ add(interaction, { first, second }) {
143
+ return interaction.sendMessage({ content: `The result is: ${first + second}` });
144
+ }
145
+
146
+ subtract(interaction, { first, second }) {
147
+ return interaction.sendMessage({ content: `The result is: ${first - second}` });
148
+ }
149
+ }
150
+ ```
151
+
152
+ > **Note**: this is an alternative to the decorators, not a replacement — both approaches share the same underlying
153
+ > registry and can be mixed across different commands in the same project.
154
+
155
+ ### Utility decorators
156
+
157
+ The utility decorators for configuring pieces and gating methods — `ApplyOptions`, `RequiresGuildContext`,
158
+ `RequiresDMContext`, `RequiresUserPermissions`, `RequiresClientPermissions`, `Enumerable`, `EnumerableMethod`, and the
159
+ `createClassDecorator`/`createMethodDecorator`/`createProxy`/`createFunctionPrecondition` primitives — live in their own
160
+ package, [`@wolfstar/decorators`](https://npmx.dev/package/@wolfstar/decorators), the same way `@sapphire/framework`
161
+ pairs with `@sapphire/decorators`:
162
+
163
+ ```typescript
164
+ import { ApplyOptions, RequiresGuildContext } from '@wolfstar/decorators';
165
+ import { Command, RegisterCommand } from '@wolfstar/http-framework';
166
+
167
+ @ApplyOptions<Command.Options>({ name: 'kick' })
168
+ @RegisterCommand({ name: 'kick', description: 'Kicks a member' })
169
+ export class UserCommand extends Command {
170
+ @RequiresGuildContext()
171
+ public override chatInputRun(interaction: Command.ChatInputInteraction) {
172
+ return interaction.reply({ content: `Hello from ${interaction.guildId}!` });
173
+ }
174
+ }
175
+ ```
176
+
177
+ ### Client
178
+
179
+ The `Client` class contains the HTTP server, powered by [`node:http`], it also registers a handler that processes whether or not the HTTP request comes from Discord and processes the information accordingly, handling the heavyweight in the background.
180
+
181
+ ```typescript
182
+ import { Client } from '@wolfstar/http-framework';
183
+
184
+ const client = new Client({
185
+ discordToken: process.env.DISCORD_TOKEN,
186
+ discordPublicKey: process.env.DISCORD_PUBLIC_KEY
187
+ });
188
+
189
+ // Load all the commands and message component handlers:
190
+ await client.load();
191
+
192
+ // Start up the HTTP server;
193
+ await client.listen({ port: 3000 });
194
+ ```
195
+
196
+ `Client#fetch` is the Web `Request`/`Response` counterpart of `listen()` — the same signature verification, routing
197
+ and replies, without binding a port. Use it to serve interactions from anything that speaks Fetch instead of
198
+ `node:http` (Nitro, a Worker, `Bun.serve`, `Deno.serve`, Vite's own dev middleware):
199
+
200
+ ```typescript
201
+ export default {
202
+ fetch: (request: Request) => client.fetch(request, { postPath: '/interactions' })
203
+ };
204
+ ```
205
+
206
+ This is what `@wolfstar/cli`'s `experimental.enableNitro` uses under the hood: the generated Nitro server entry calls
207
+ `client.fetch(request)` directly instead of `listen()`, so `node:http` is swapped for whatever preset Nitro targets.
208
+
209
+ ### Logger
210
+
211
+ The framework ships a minimal logger, available as `container.logger` (and as `client.logger`) as soon as
212
+ `@wolfstar/http-framework` is imported. It writes to the matching `console` method and filters entries by
213
+ `LogLevel`, which defaults to `LogLevel.Info`:
214
+
215
+ ```typescript
216
+ import { container, Client, LogLevel } from '@wolfstar/http-framework';
217
+
218
+ const client = new Client({ logger: { level: LogLevel.Debug } });
219
+
220
+ container.logger.info('Ready');
221
+ container.logger.debug('Interaction received', interaction.id);
222
+ ```
223
+
224
+ The built-in implementation is intentionally bare: it has no timestamps, colours, or transports. Those belong to a
225
+ logger plugin, which replaces it by assigning an `ILogger` to `options.logger.instance` from a
226
+ `preGenericsInitialization` hook:
227
+
228
+ ```typescript
229
+ import { Plugin, preGenericsInitialization, type ClientOptions } from '@wolfstar/http-framework';
230
+
231
+ export class LoggerPlugin extends Plugin {
232
+ public static [preGenericsInitialization](options: ClientOptions): void {
233
+ options.logger ??= {};
234
+ options.logger.instance = new MyLogger(options.logger);
235
+ }
236
+ }
237
+ ```
238
+
239
+ Because the plugin only has to satisfy the `ILogger` interface, the rest of the framework — including Hot Module
240
+ Reloading and the command router — keeps logging through `container.logger` without any change.
241
+
242
+ ### Client events
243
+
244
+ The `Client` extends an event emitter typed by the `ClientEvents` interface. Every event name is also available as a
245
+ member of the `Events` enum, which is the recommended way to reference them, as the plain strings remain valid:
246
+
247
+ ```typescript
248
+ import { Events } from '@wolfstar/http-framework';
249
+
250
+ client.on(Events.CommandError, (error, context) => {
251
+ console.error(`Failed to run ${context.command.name}`, error);
252
+ });
253
+ ```
254
+
255
+ | Enum member | Event name | Arguments |
256
+ | -------------------------------------- | ------------------------------- | --------------------------------------------- |
257
+ | `Events.Error` | `error` | `error: unknown` |
258
+ | `Events.PluginLoaded` | `pluginLoaded` | `hook: PluginHook, name: string \| undefined` |
259
+ | `Events.CommandNameMissing` | `commandNameMissing` | `interaction, response` |
260
+ | `Events.CommandNameUnknown` | `commandNameUnknown` | `interaction, response` |
261
+ | `Events.CommandMethodUnknown` | `commandMethodUnknown` | `context` |
262
+ | `Events.CommandRun` | `commandRun` | `context` |
263
+ | `Events.CommandSuccess` | `commandSuccess` | `context, value: unknown` |
264
+ | `Events.CommandError` | `commandError` | `error: unknown, context` |
265
+ | `Events.CommandFinish` | `commandFinish` | `context` |
266
+ | `Events.AutocompleteRun` | `autocompleteRun` | `context` |
267
+ | `Events.AutocompleteSuccess` | `autocompleteSuccess` | `context, value: unknown` |
268
+ | `Events.AutocompleteError` | `autocompleteError` | `error: unknown, context` |
269
+ | `Events.AutocompleteFinish` | `autocompleteFinish` | `context` |
270
+ | `Events.InteractionHandlerNameInvalid` | `interactionHandlerNameInvalid` | `interaction, response` |
271
+ | `Events.InteractionHandlerNameUnknown` | `interactionHandlerNameUnknown` | `interaction, response` |
272
+ | `Events.InteractionHandlerRun` | `interactionHandlerRun` | `context` |
273
+ | `Events.InteractionHandlerSuccess` | `interactionHandlerSuccess` | `context, value: unknown` |
274
+ | `Events.InteractionHandlerError` | `interactionHandlerError` | `error: unknown, context` |
275
+ | `Events.InteractionHandlerFinish` | `interactionHandlerFinish` | `context` |
276
+
277
+ The Hot Module Reloading events are listed in [their own section](#hot-module-reloading).
278
+
279
+ Listeners declared as pieces can use the enum too:
280
+
281
+ ```typescript
282
+ import { ApplyOptions } from '@wolfstar/decorators';
283
+ import { Events, Listener } from '@wolfstar/http-framework';
284
+
285
+ @ApplyOptions<Listener.Options>({ event: Events.CommandError })
286
+ export class UserListener extends Listener {
287
+ public run(error: unknown, context: ClientEventCommandContext) {
288
+ console.error(`Failed to run ${context.command.name}`, error);
289
+ }
290
+ }
291
+ ```
292
+
293
+ ### Hot Module Reloading
294
+
295
+ `@wolfstar/http-framework` ships with Hot Module Reloading (HMR) as a core feature, no plugin required. When enabled,
296
+ every path registered in every store is watched, and pieces are loaded, reloaded, and unloaded in place as their files
297
+ are created, changed, and deleted, without restarting the process.
298
+
299
+ ```typescript
300
+ import { Client } from '@wolfstar/http-framework';
301
+
302
+ const client = new Client({
303
+ discordToken: process.env.DISCORD_TOKEN,
304
+ discordPublicKey: process.env.DISCORD_PUBLIC_KEY,
305
+ // Development only, do not enable this in production:
306
+ hmr: { enabled: process.env.NODE_ENV !== 'production' }
307
+ });
308
+
309
+ // `load` starts the reloader once the stores have been loaded:
310
+ await client.load();
311
+ ```
312
+
313
+ The `hmr` option accepts [all of chokidar's options], plus:
314
+
315
+ | Option | Default | Description |
316
+ | --------- | ------- | ------------------------------------------------------------------------------------------ |
317
+ | `enabled` | `true` | Whether HMR is started. Omitting the `hmr` option entirely also leaves the reloader unset. |
318
+ | `silent` | `false` | Whether the reloader refrains from writing to the console. Events are always emitted. |
319
+
320
+ The reloader is exposed as `client.hmr`, which is `null` when HMR is disabled, and can be stopped at any time:
321
+
322
+ ```typescript
323
+ await client.hmr?.stop();
324
+ ```
325
+
326
+ It can also be used standalone, without a `Client`, as long as the stores are registered in the container:
327
+
328
+ ```typescript
329
+ import { HotModuleReloader } from '@wolfstar/http-framework';
330
+
331
+ const reloader = await new HotModuleReloader({ silent: true }).start();
332
+ ```
333
+
334
+ The client emits an event for every operation, which is useful to react to changes, for example to push the updated
335
+ application commands to Discord while developing:
336
+
337
+ ```typescript
338
+ client.on(Events.HmrPieceReloaded, async (piece) => {
339
+ if (piece.store.name === 'commands') {
340
+ await client.registry.pushGlobalCommandsInGuild(process.env.DEVELOPMENT_GUILD_ID);
341
+ }
342
+ });
343
+ ```
344
+
345
+ | Enum member | Event | Arguments | Description |
346
+ | ------------------------- | ------------------ | ----------------------- | ---------------------------------------------------- |
347
+ | `Events.HmrStart` | `hmrStart` | `paths: string[]` | The reloader started watching the given store paths. |
348
+ | `Events.HmrStop` | `hmrStop` | — | The reloader stopped and closed all of its watchers. |
349
+ | `Events.HmrPiecesLoaded` | `hmrPiecesLoaded` | `pieces: Piece[], path` | A new file was created and its pieces were loaded. |
350
+ | `Events.HmrPieceReloaded` | `hmrPieceReloaded` | `piece: Piece, path` | An existing file changed and its piece was reloaded. |
351
+ | `Events.HmrPieceUnloaded` | `hmrPieceUnloaded` | `piece: Piece, path` | A file was deleted and its piece was unloaded. |
352
+ | `Events.HmrError` | `hmrError` | `error: unknown, path` | An operation failed; saving the file again retries. |
353
+
354
+ > **Note**: unloading a command also removes its entry from the `ApplicationCommandRegistry`, so a reloaded command is
355
+ > registered exactly once. HMR does not push the updated commands to Discord on its own, subscribe to the events above
356
+ > if you want that behaviour.
357
+
358
+ ### Project configuration (`stars.config.*`)
359
+
360
+ The typed project configuration consumed by the [`stars` CLI](../cli) — the `defineConfig` helper and the config
361
+ loader — lives in [`@wolfstar/schema`](../schema), a small package shared by `@wolfstar/http-framework`
362
+ and `@wolfstar/cli` so neither depends on the other's runtime. `@wolfstar/http-framework` re-exports its public
363
+ surface unchanged as `@wolfstar/http-framework/config`, so any tool can resolve a project's configuration without
364
+ pulling in `@wolfstar/cli` — most projects should keep importing it from here rather than depending on
365
+ `@wolfstar/schema` directly.
366
+
367
+ The package also exposes `@wolfstar/http-framework/schema`, forwarding the shared schema's types and loader,
368
+ like Nuxt's `nuxt/schema`. Both facades preserve the same exports and function identities; existing `/config`
369
+ imports remain supported. Tooling can read the package metadata through `@wolfstar/http-framework/package.json`.
370
+
371
+ ```typescript
372
+ import type { StarsConfig, ResolvedStarsConfig } from '@wolfstar/http-framework/schema';
373
+ ```
374
+
375
+ Installing `@wolfstar/http-framework` also gives a project the `stars` binary (it depends on `@wolfstar/cli` and
376
+ exposes it as `stars`, the way `nuxt` exposes `nuxi`'s binary), so a project needs no separate `@wolfstar/cli`
377
+ install to run `stars dev`/`stars build`.
378
+
379
+ ```typescript
380
+ // stars.config.ts
381
+ import { defineConfig } from '@wolfstar/http-framework/config';
382
+
383
+ export default defineConfig({});
384
+ ```
385
+
386
+ `@wolfstar/http-framework/config` has no side effects — importing it (or a `stars.config.ts` that imports it) never
387
+ starts the bot. `loadStarsConfig` discovers `stars.config.{ts,mts,cts,js,mjs,cjs}` from a directory, applies defaults,
388
+ validates every option and resolves all paths to absolute ones.
389
+
390
+ `dev.url`, the URL `stars dev` shows and health-checks the bot on, needs no configuration either: it is detected the
391
+ way Vite's and Nuxt's dev servers are, from `HTTP_PORT` (env var, `src/.env*`/`.env*`, or `dev.env`) or `3000`, and
392
+ `localhost` is swapped for `127.0.0.1` at runtime if that is what is actually reachable. Set `dev.url` explicitly only
393
+ to override it, e.g. for a LAN address: `dev: { url: 'http://192.168.1.5:3000' }`.
394
+
395
+ `dev.banner` replaces the default Stars wordmark in the interactive CLI. Use a string or an array of lines, for
396
+ example `dev: { banner: ['★ STARYL', 'Twitch notifications'] }`, or `false` to hide it. Up to four lines fit the
397
+ compact panel (`dev.layout: 'panel'`); the dashboard, which is what a roomy terminal gets, shows the project's name
398
+ instead. Application logs and standalone banners go to the log stream, not over the dev UI.
399
+
400
+ `dev` also carries optional overrides for the dev loop. Press `t` in the TUI to toggle a quick tunnel without any
401
+ configuration:
402
+
403
+ ```typescript
404
+ export default defineConfig({
405
+ dev: {
406
+ // A type checker next to the bot, reported on the dev UI's `types` channel. Never blocks a build.
407
+ // `checker` is 'tsc' | 'golar' | 'tsz' | 'auto' (default: golar when installed, tsc otherwise).
408
+ typecheck: { checker: 'golar' },
409
+ // A cloudflared quick tunnel so Discord can reach the interactions endpoint, or an https URL you serve.
410
+ tunnel: true,
411
+ // Where the session's logs are mirrored, so a run can be read after the terminal UI is gone.
412
+ logFile: '.stars/dev.log',
413
+ // 'auto' (default): the full-screen dashboard on a terminal of at least 90x20, the compact panel otherwise.
414
+ layout: 'auto',
415
+ // What the dev UI shows at start; `dir` adds one log file per run next to `logFile`, keeping the last `keep`.
416
+ logs: { channels: ['bot', 'commands', 'interactions'], levels: ['error', 'warn', 'info'], dir: 'logs', keep: 10 },
417
+ // When the bot's application commands change: 'prompt' (default) asks before redeploying, 'auto', or 'off'.
418
+ commands: { refresh: 'prompt' },
419
+ // `false` restarts the bot on every change, even when the bot hot reloads its pieces (`hmr` on the Client).
420
+ hmr: true
421
+ }
422
+ });
423
+ ```
424
+
425
+ With the `hmr` option of the `Client` enabled, a change that only touches pieces is left to the bot: `stars dev` does
426
+ not restart it, and the `hmr` channel of the dev UI shows what was reloaded. The channels that report what happens
427
+ inside the bot (`hmr`, `commands`, `interactions`, `http`, `lifecycle`) are fed by the bot itself, see
428
+ [`stars dev`](../cli#stars-dev).
429
+
430
+ `tunnel.updateEndpoint` writes the public URL to the Discord application's `interactions_endpoint_url`; it is opt-in
431
+ because it edits a live application, and needs `DISCORD_TOKEN` in the environment or the project's `.env`.
432
+
433
+ ### The build (`tsdown`)
434
+
435
+ `tsdown` is the bundler `stars build` and `stars dev` use, and it is configured from `stars.config` itself. A base
436
+ project configures nothing at all — this is a complete build:
437
+
438
+ ```typescript
439
+ export default defineConfig({});
440
+ ```
441
+
442
+ The defaults are the configuration a bot would otherwise write out by hand:
443
+
444
+ | Option | Default | Why |
445
+ | -------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------- |
446
+ | `entry` | every source file next to `entry`, minus `*.test.*`/`*.spec.*` | pieces are found by the stores, not imported by the entry |
447
+ | `unbundle` | `true` | keeps `dist/commands/…` loadable one file at a time at runtime |
448
+ | `format` / `platform` | `'esm'` / `'node'` | what the framework and `node dist/…` expect |
449
+ | `outDir` / `outExtensions` | `build.outDir` / the extension of `build.output` | so `stars dev`, `package.json#main` and `node dist/…` agree |
450
+ | `tsconfig` | `build.tsconfig` (`src/tsconfig.json`, else `tsconfig.json`) | `tsdown` alone looks only next to the root, missing the `src/` layout |
451
+ | `sourcemap` / `treeshake` | `true` | a deployed bot needs readable stack traces |
452
+ | `minify` | `false` | nothing is shipped over a wire, so bytes buy nothing here |
453
+ | `deps.neverBundle` | `true` | dependencies stay in `node_modules` instead of being copied into `dist` |
454
+ | `alias` | `~`/`@` → the entry's directory, `~~`/`@@` → the project root | the prefixes Nuxt gives every project |
455
+ | `dts` | `false` | nothing consumes a bot's `dist/`; set `tsdown: { dts: true }` if yours does |
456
+
457
+ #### Aliases
458
+
459
+ The four prefixes Nuxt gives every project work out of the box, pointing at the same two places its own do:
460
+
461
+ ```typescript
462
+ import { Greeting } from '~/lib/greeting'; // and '@/lib/greeting' — the entry's directory
463
+ import pkg from '~~/package.json'; // and '@@/package.json' — the project root
464
+ ```
465
+
466
+ The build resolves them on its own; TypeScript needs the matching `paths` (the scaffold writes them, and
467
+ `examples/basic` shows them):
468
+
469
+ ```jsonc
470
+ {
471
+ "compilerOptions": {
472
+ "paths": {
473
+ "~/*": ["./src/*"],
474
+ "@/*": ["./src/*"],
475
+ "~~/*": ["./*"],
476
+ "@@/*": ["./*"]
477
+ }
478
+ }
479
+ }
480
+ ```
481
+
482
+ A project's own `tsdown.alias` is added to these rather than replacing them, and a target written as a relative path
483
+ (`'./src/lib'`) is resolved against the project root, the way every other path in `stars.config` is — a module id
484
+ (`'preact/compat'`) is left alone.
485
+
486
+ So the block is for what the defaults cannot know — an extra alias, declaration output, or a target. Conventional
487
+ `src/locales` assets are copied to `dist/locales` automatically by the CLI:
488
+
489
+ ```typescript
490
+ export default defineConfig({
491
+ tsdown: { alias: { '#shared': './src/shared' }, dts: true }
492
+ });
493
+ ```
494
+
495
+ Anything in `tsdown` wins over the defaults, and `plugins` are appended rather than replaced.
496
+
497
+ With `future.compatibilityVersion: 3` (end-of-life legacy mode) a `tsdown.config.*` in the project root is still
498
+ loaded and `tsdown` is merged over it, so a project can move its options across one at a time. From `4` on the block is
499
+ the whole configuration, and a leftover `tsdown.config.*` is reported instead of being silently ignored.
500
+
501
+ `vite: {}` works the same way for `build.tool: 'vite'` (see [experimental flags](#experimental-flags)): it is merged
502
+ into the project's own `vite.config.*`, the way `vite: {}` in a Nuxt config is.
503
+
504
+ ### Compatibility version
505
+
506
+ `future.compatibilityVersion` selects the build-default generation. Version 5 is the default and version 4 remains
507
+ supported. Version 3 is **end-of-life**: it still works for projects that have a standalone `tsdown.config.*`, but
508
+ every command prints a `COMPATIBILITY_VERSION_EOL` warning, and it is removed in the next major.
509
+
510
+ ```typescript
511
+ export default defineConfig({
512
+ future: {
513
+ compatibilityVersion: 4
514
+ }
515
+ });
516
+ ```
517
+
518
+ | Version | What it changes |
519
+ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
520
+ | `3` (EOL) | Legacy behaviour: a `tsdown.config.*` drives the build, auto imports off unless asked for |
521
+ | `4` | `tsdown` configured from `stars.config` alone, auto imports on and wired in, `'auto'` picks `tsdown` for TypeScript; [`env`](#environment-env) is opt-in |
522
+ | `5` (default) | Everything in `4`, plus [`env`](#environment-env) registered automatically when the project depends on `@wolfstar/env-utilities` |
523
+
524
+ From version 4 on:
525
+
526
+ - **Auto imports are on** with the `tsdown` build tool, and the `autoImports()` plugin is wired into the build by
527
+ `stars` itself instead of by the project's own configuration file — the framework's exports and the project's
528
+ `src/lib/**`, `src/utils/**` are usable without an `import` statement, the same way Nuxt's own are.
529
+ - **`tsdown` is configured from `stars.config` alone.** A `tsdown.config.*` (or a `package.json#tsdown` field) raises
530
+ `TSDOWN_CONFIG_FILE_UNSUPPORTED`, because a build that quietly dropped the plugins such a file declares would be
531
+ far harder to diagnose than an error naming it.
532
+ - **`build.tool: 'auto'` resolves to `tsdown`** for any TypeScript entry, rather than looking for a `tsdown.config.*`
533
+ or a `tsdown` dependency first. `tsc` stays available as an explicit choice.
534
+
535
+ Version 5 adds one thing: **the environment is registered for you.** `stars` calls `setup()` from
536
+ `@wolfstar/env-utilities` before any module of the bot runs, so the bot no longer loads its `.env*` files by hand.
537
+
538
+ `stars info` prints the version in effect, and any warning about it.
539
+
540
+ ### Environment (`env`)
541
+
542
+ `env` mirrors the options of `setup()` from `@wolfstar/env-utilities`. `stars` writes them into the built entry and
543
+ calls `setup()` with them as the entry's very first import — before `@wolfstar/plugin-*` registrations and the bot's
544
+ own modules, all of which may read `process.env` as soon as they load.
545
+
546
+ ```typescript
547
+ export default defineConfig({
548
+ env: { prefix: 'BOT_', loader: 'varlock' }
549
+ });
550
+ ```
551
+
552
+ | Option | Type | Default |
553
+ | ---------- | --------------------------------- | ------------------------- |
554
+ | `enabled` | `boolean` | see below |
555
+ | `path` | `string` | `src/.env*`, then `.env*` |
556
+ | `env` | `string` | `NODE_ENV` |
557
+ | `prefix` | `string` | none |
558
+ | `loader` | `'node' \| 'dotenv' \| 'varlock'` | detected |
559
+ | `debug` | `boolean` | `false` |
560
+ | `encoding` | `string` | `'utf8'` |
561
+
562
+ - `env: false` turns the registration off; `env: true` turns it on with the defaults. Any option at all is an opt-in
563
+ as well, which requires `@wolfstar/env-utilities` in the project's dependencies (`ENV_REQUIRES_ENV_UTILITIES`).
564
+ - Without an `env` block it is on from compatibility version 5, when the project depends on
565
+ `@wolfstar/env-utilities`, and off with `experimental.enableNitro` — serverless presets have no `.env` files to load.
566
+ - Only the serializable options are mirrored: they end up in the build output as JSON. `path` stays relative and is
567
+ resolved against the bot's working directory at runtime, so the output does not depend on the machine that built it.
568
+ - tsdown, Vite and Nitro builds register it through the entry transform. `build.tool: 'tsc'` and `'none'` never pass
569
+ through it: `stars dev` preloads it with `node --import`, but in production such a bot loads its environment itself
570
+ (or runs with `node --import @wolfstar/env-utilities/setup`). The projects `@wolfstar/create-http-framework`
571
+ generates for them set `env: false` and keep calling `setup()` in `src/lib/setup`.
572
+ - `stars dev` reads `HTTP_PORT` for the default `dev.url` from the same `path`/`env` files, after `env:options`. With
573
+ `loader: 'varlock'` it reads it, and the tunnel's Discord credentials, from `varlock load` in the project instead —
574
+ run the way the bot loads it, without `env.env`, which varlock ignores.
575
+
576
+ ### Hooks
577
+
578
+ `hooks` registers lifecycle hooks of the `stars` CLI, run with [`hookable`](https://github.com/unjs/hookable) the way
579
+ Nuxt's own `hooks` are. Each hook is keyed by its full name or nested under its namespace, as a function or an array
580
+ of functions:
581
+
582
+ ```typescript
583
+ export default defineConfig({
584
+ hooks: {
585
+ 'env:options'(options) {
586
+ if (process.env.CI) options.path = '.env.ci';
587
+ },
588
+ build: {
589
+ done(outcome) {
590
+ if (!outcome.ok) console.error(outcome.message);
591
+ }
592
+ }
593
+ }
594
+ });
595
+ ```
596
+
597
+ | Hook | Arguments | Runs |
598
+ | ----------------- | ------------------- | --------------------------------------------------------------------- |
599
+ | `config:resolved` | `config` | after `stars.config` is loaded and validated, in every command |
600
+ | `env:options` | `options`, `config` | before `env` is written into the build; mutate `options` to change it |
601
+ | `prepare:before` | `config` | before `.stars/` is generated |
602
+ | `prepare:done` | `config`, `result` | after `.stars/` is generated |
603
+ | `builder:created` | `builder`, `config` | once the builder for `build.tool` exists, before it builds |
604
+ | `tsdown:options` | `options`, `config` | before `tsdown.build()`; mutate `options` to change the build |
605
+ | `build:before` | `config` | before each build (every rebuild in `stars dev`) |
606
+ | `build:done` | `outcome`, `config` | after each build, successful or not (`outcome.ok`) |
607
+ | `dev:start` | `config` | once `stars dev` is watching |
608
+ | `dev:restart` | `reason`, `config` | before `stars dev` (re)starts the bot |
609
+ | `dev:close` | `config` | when `stars dev` shuts down |
610
+
611
+ Hooks run in the CLI process, never in the bot — `env:options` is how a hook changes what the bot receives. An unknown
612
+ hook name fails with `UNKNOWN_HOOK` rather than registering a hook that never runs. In `stars dev` a hook that throws
613
+ is logged and the watcher keeps going; in `stars build` it fails the build.
614
+
615
+ `stars build` and `stars dev` run them in the same order: `config:resolved` → `env:options` → `builder:created` →
616
+ `prepare:before` → `prepare:done`, then `build:before` → `build:done` for each build. `stars dev` adds `dev:start` once
617
+ it is watching, `dev:restart` before each (re)start of the bot, and `dev:close` on shutdown. `stars build` awaits every
618
+ hook. In `stars dev` the builder drives rebuilds on its own: `build:before` starts when it reports a build, without
619
+ delaying it, while `build:done`, `dev:restart` and `dev:close` run one after another — a slow `build:done` settles before
620
+ the restart it triggered, and before shutdown. `build:done` gets the final outcome, including a failure to copy
621
+ `src/locales`.
622
+
623
+ ### Experimental flags
624
+
625
+ `experimental` is the same kind of block Nuxt's own `experimental` is: opt-in booleans, all `false` by default, each
626
+ guarding work that is still landing.
627
+
628
+ ```typescript
629
+ export default defineConfig({
630
+ entry: 'src/main.ts',
631
+ // `build.tool: 'vite'` is only accepted with `enableVite`, and `'auto'` only then detects a vite.config.*
632
+ build: { tool: 'vite' },
633
+ experimental: {
634
+ // Vite as the build tool and the HTTP server, in place of tsdown plus the framework's node:http listener.
635
+ enableVite: true,
636
+ // The project runs Vite itself: `stars dev` only watches the output and restarts the bot.
637
+ enableExternalVite: false,
638
+ // Build and serve through Nitro (itself a Vite plugin) instead of node:http, deployable to any of its
639
+ // presets; the entry's default export must be the `Client` instance, served through `client.fetch(request)`.
640
+ enableNitro: false
641
+ }
642
+ });
643
+ ```
644
+
645
+ The resolved configuration is also available programmatically:
646
+
647
+ ```typescript
648
+ import { loadStarsConfig } from '@wolfstar/http-framework/config';
649
+
650
+ const config = await loadStarsConfig({ cwd: process.cwd() });
651
+ console.log(config.entry, config.build.output);
652
+ ```
653
+
654
+ Invalid options raise a `ConfigError` with a stable `code`, the offending option `path`, the `file` it came from, and
655
+ an actionable `hint`. See the [`@wolfstar/cli` README](../cli#configuration) for the full option reference and how the
656
+ `stars` commands (`dev`, `build`, `info`, `codegen`, `prepare`, `commands`) use it.
657
+
658
+ ### ApplicationCommandRegistry
659
+
660
+ The `ApplicationCommandRegistry` is `@wolfstar/http-framework`'s centralized registry and uses [`@discordjs/rest`] to register them in Discord.
661
+
662
+ ```typescript
663
+ // Assuming you have the code above, and that you called `client.load()`:
664
+
665
+ // Register all global commands:
666
+ await client.registry.pushGlobalCommands();
667
+
668
+ // Register all the guild-restricted commands:
669
+ await client.registry.pushGuildRestrictedCommands();
670
+ ```
671
+
672
+ However, if you want to use the registry without the client, you can do so:
673
+
674
+ ```typescript
675
+ import { applicationCommandRegistry } from '@wolfstar/http-framework';
676
+ import { REST } from '@discordjs/rest';
677
+
678
+ applicationCommandRegistry.setup({
679
+ rest: new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN),
680
+ clientId: process.env.DISCORD_CLIENT_ID
681
+ });
682
+
683
+ // Load all the commands:
684
+ await applicationCommandRegistry.loadCommands();
685
+
686
+ // Register all global commands:
687
+ await applicationCommandRegistry.pushGlobalCommands();
688
+
689
+ // Register all the guild-restricted commands:
690
+ await applicationCommandRegistry.pushGuildRestrictedCommands();
691
+ ```
692
+
693
+ > **Note**: calling `applicationCommandRegistry.setup()` is not needed if you are using the `Client` class because it is
694
+ > already called automatically for you.
695
+
696
+ [all of chokidar's options]: https://github.com/paulmillr/chokidar#api
697
+ [`node:http`]: https://nodejs.org/api/http.html
698
+ [`@discordjs/rest`]: https://www.npmjs.com/package/@discordjs/rest
699
+ [`@sapphire/pieces`]: https://www.npmjs.com/package/@sapphire/pieces
700
+ [`@sapphire/framework`]: https://sapphirejs.dev/docs/Guide/commands/application-commands/application-command-registry/