@seedcord/core 0.1.0-next.5 → 0.1.0-next.7

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.
Files changed (66) hide show
  1. package/dist/{AutocompleteOptions-eB87SZ4a.d.mts → AutocompleteOptions-DLp_B8as.d.mts} +66 -124
  2. package/dist/HmrModuleHandler-3sNgKHdf.mjs +163 -0
  3. package/dist/HmrModuleHandler-3sNgKHdf.mjs.map +1 -0
  4. package/dist/HmrModuleHandler-CTBng7wV.cjs +170 -0
  5. package/dist/HmrModuleHandler-CTBng7wV.cjs.map +1 -0
  6. package/dist/Plugin-3rlH0uxH.d.mts +620 -0
  7. package/dist/Plugin-Cgp1a0uQ.cjs +123 -0
  8. package/dist/Plugin-Cgp1a0uQ.cjs.map +1 -0
  9. package/dist/Plugin-j9GYdroS.mjs +106 -0
  10. package/dist/Plugin-j9GYdroS.mjs.map +1 -0
  11. package/dist/{permissions-DQL5ViZn.cjs → Silence-DiP95iwD.cjs} +54 -305
  12. package/dist/Silence-DiP95iwD.cjs.map +1 -0
  13. package/dist/{permissions-SokZLJMj.mjs → Silence-dbC2slOB.mjs} +47 -238
  14. package/dist/Silence-dbC2slOB.mjs.map +1 -0
  15. package/dist/basic-D7bDlgSP.d.mts +10 -0
  16. package/dist/devChannel-CQU7hq7Q.mjs +14 -0
  17. package/dist/devChannel-CQU7hq7Q.mjs.map +1 -0
  18. package/dist/devChannel-CoCmAVDx.cjs +26 -0
  19. package/dist/devChannel-CoCmAVDx.cjs.map +1 -0
  20. package/dist/hmr.index.cjs +2 -164
  21. package/dist/hmr.index.mjs +2 -162
  22. package/dist/index-Dueqztyy.d.mts +142 -0
  23. package/dist/index.cjs +53 -72
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.mts +7 -4
  26. package/dist/index.mjs +9 -35
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/internal.index.cjs +267 -89
  29. package/dist/internal.index.cjs.map +1 -1
  30. package/dist/internal.index.d.mts +68 -3
  31. package/dist/internal.index.mjs +218 -48
  32. package/dist/internal.index.mjs.map +1 -1
  33. package/dist/node-internal.index.cjs +716 -0
  34. package/dist/node-internal.index.cjs.map +1 -0
  35. package/dist/node-internal.index.d.cts +1 -0
  36. package/dist/node-internal.index.d.mts +261 -0
  37. package/dist/node-internal.index.mjs +706 -0
  38. package/dist/node-internal.index.mjs.map +1 -0
  39. package/dist/phases-BfpAG6w3.mjs +31 -0
  40. package/dist/phases-BfpAG6w3.mjs.map +1 -0
  41. package/dist/phases-C_BGNyVo.cjs +43 -0
  42. package/dist/phases-C_BGNyVo.cjs.map +1 -0
  43. package/dist/plugin.index.cjs +4 -0
  44. package/dist/plugin.index.d.cts +1 -0
  45. package/dist/plugin.index.d.mts +3 -0
  46. package/dist/plugin.index.mjs +3 -0
  47. package/dist/publishDefault-CCf1azzW.mjs +264 -0
  48. package/dist/publishDefault-CCf1azzW.mjs.map +1 -0
  49. package/dist/publishDefault-l0NQ-SHo.cjs +353 -0
  50. package/dist/publishDefault-l0NQ-SHo.cjs.map +1 -0
  51. package/dist/subscribers-B1G6_LlK.cjs +517 -0
  52. package/dist/subscribers-B1G6_LlK.cjs.map +1 -0
  53. package/dist/subscribers-DYqFVP1X.mjs +480 -0
  54. package/dist/subscribers-DYqFVP1X.mjs.map +1 -0
  55. package/package.json +25 -13
  56. package/dist/hmr.index.cjs.map +0 -1
  57. package/dist/hmr.index.mjs.map +0 -1
  58. package/dist/is-never-Ddaba1Su.d.mts +0 -58
  59. package/dist/node.index.cjs +0 -521
  60. package/dist/node.index.cjs.map +0 -1
  61. package/dist/node.index.d.cts +0 -1
  62. package/dist/node.index.d.mts +0 -2391
  63. package/dist/node.index.mjs +0 -513
  64. package/dist/node.index.mjs.map +0 -1
  65. package/dist/permissions-DQL5ViZn.cjs.map +0 -1
  66. package/dist/permissions-SokZLJMj.mjs.map +0 -1
@@ -1,7 +1,8 @@
1
- import { t as IsNever } from "./is-never-Ddaba1Su.mjs";
1
+ import { t as Constructor } from "./basic-D7bDlgSP.mjs";
2
+ import { A as Notice, N as AckState, W as InteractionRoutes, j as ReplyTelemetry, y as CoreBase } from "./Plugin-3rlH0uxH.mjs";
2
3
  import { APIMessageTopLevelComponent, APIModalInteractionResponseCallbackData, ApplicationCommandType, ChannelType, Snowflake } from "discord-api-types/v10";
3
4
  import { ActionRowBuilder, ButtonBuilder, ChannelSelectMenuBuilder, CheckboxBuilder, CheckboxGroupBuilder, CheckboxGroupOptionBuilder, ContainerBuilder, ContextMenuCommandBuilder, EmbedBuilder, FileBuilder, FileUploadBuilder, LabelBuilder, MediaGalleryBuilder, MentionableSelectMenuBuilder, ModalBuilder, RadioGroupBuilder, RadioGroupOptionBuilder, RoleSelectMenuBuilder, SectionBuilder, SeparatorBuilder, SlashCommandBuilder, SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextDisplayBuilder, TextInputBuilder, UserSelectMenuBuilder } from "@discordjs/builders";
4
- import { Config, DeferOpts, IRateLimiter, RenderContext, ReplyResponse, SendOpts } from "@seedcord/types";
5
+ import { DeferOpts, ReplyResponse, SendOpts } from "@seedcord/types";
5
6
 
6
7
  //#region src/registries/SlashOptionRegistry.d.ts
7
8
  /** A slash option kind as a plain string union (`string`, `channel`, ...), keyed to the registry. */
@@ -151,13 +152,61 @@ declare abstract class RowComponent<RowKey extends RowType> extends BaseComponen
151
152
  get component(): InstantiatedActionRow<RowKey>;
152
153
  }
153
154
  //#endregion
154
- //#region ../../node_modules/.pnpm/type-fest@5.7.0/node_modules/type-fest/source/basic.d.ts
155
+ //#region ../../node_modules/.pnpm/type-fest@5.7.0/node_modules/type-fest/source/is-never.d.ts
155
156
  /**
156
- Matches a [`class` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes).
157
+ Returns a boolean for whether the given type is `never`.
157
158
 
158
- @category Class
159
+ @link https://github.com/microsoft/TypeScript/issues/31751#issuecomment-498526919
160
+ @link https://stackoverflow.com/a/53984913/10292952
161
+ @link https://www.zhenghao.io/posts/ts-never
162
+
163
+ Useful in type utilities, such as checking if something does not occur.
164
+
165
+ @example
166
+ ```
167
+ import type {IsNever, And} from 'type-fest';
168
+
169
+ type A = IsNever<never>;
170
+ //=> true
171
+
172
+ type B = IsNever<any>;
173
+ //=> false
174
+
175
+ type C = IsNever<unknown>;
176
+ //=> false
177
+
178
+ type D = IsNever<never[]>;
179
+ //=> false
180
+
181
+ type E = IsNever<object>;
182
+ //=> false
183
+
184
+ type F = IsNever<string>;
185
+ //=> false
186
+ ```
187
+
188
+ @example
189
+ ```
190
+ import type {IsNever} from 'type-fest';
191
+
192
+ type IsTrue<T> = T extends true ? true : false;
193
+
194
+ // When a distributive conditional is instantiated with `never`, the entire conditional results in `never`.
195
+ type A = IsTrue<never>;
196
+ //=> never
197
+
198
+ // If you don't want that behaviour, you can explicitly add an `IsNever` check before the distributive conditional.
199
+ type IsTrueFixed<T> =
200
+ IsNever<T> extends true ? false : T extends true ? true : false;
201
+
202
+ type B = IsTrueFixed<never>;
203
+ //=> false
204
+ ```
205
+
206
+ @category Type Guard
207
+ @category Utilities
159
208
  */
160
- type Constructor<T, Arguments extends unknown[] = any[]> = new (...arguments_: Arguments) => T;
209
+ type IsNever<T> = [T] extends [never] ? true : false;
161
210
  //#endregion
162
211
  //#region ../../node_modules/.pnpm/type-fest@5.7.0/node_modules/type-fest/source/non-empty-tuple.d.ts
163
212
  /**
@@ -219,29 +268,6 @@ declare function RegisterCommand(scope: 'global'): (ctor: CommandCtor) => void;
219
268
  */
220
269
  declare function RegisterCommand(scope: 'guild', guilds: string[]): (ctor: CommandCtor) => void;
221
270
  //#endregion
222
- //#region src/metadataKeys.d.ts
223
- declare enum InteractionRoutes {
224
- Slash = "slash",
225
- Button = "button",
226
- Modal = "modal",
227
- StringMenu = "stringMenu",
228
- UserMenu = "userMenu",
229
- RoleMenu = "roleMenu",
230
- ChannelMenu = "channelMenu",
231
- MentionableMenu = "mentionableMenu",
232
- MessageContextMenu = "messageContextMenu",
233
- UserContextMenu = "userContextMenu",
234
- Autocomplete = "autocomplete"
235
- }
236
- declare const InteractionMetadataKey: unique symbol;
237
- declare const InteractionRouteKeys: Record<InteractionRoutes, symbol>;
238
- declare const MiddlewareMetadataKey: unique symbol;
239
- declare const EventMetadataKey: unique symbol;
240
- declare const CommandMetadataKey: unique symbol;
241
- declare const GatedMetadataKey: unique symbol;
242
- declare const SubscribeMetadataKey: unique symbol;
243
- declare const WebhookUrlMetadataKey: unique symbol;
244
- //#endregion
245
271
  //#region src/customId/Field.d.ts
246
272
  /**
247
273
  * One field in a customId shape, its wire kind plus the type it decodes to.
@@ -422,37 +448,16 @@ declare enum SelectMenuKind {
422
448
  }
423
449
  type RoutableConstructor = new (...args: never[]) => unknown;
424
450
  declare function areRoutes(routes: unknown): routes is string[];
451
+ /**
452
+ * Every stored route kind and its route strings on a decorated constructor. The build's manifest
453
+ * emitter reads the same pairs.
454
+ */
455
+ declare function interactionRoutesOf(constructor: RoutableConstructor): [InteractionRoutes, string[]][];
425
456
  declare function storeInteractionRoute(route: InteractionRoutes, routes: string | readonly string[], constructor: RoutableConstructor): void;
426
457
  declare function storeComponentRoute(route: InteractionRoutes, defs: readonly AnyCustomId[], constructor: RoutableConstructor): void;
427
458
  declare function selectMenuRouteOf(kind: SelectMenuKind): InteractionRoutes;
428
459
  declare function contextMenuRouteOf(kind: ApplicationCommandType.User | ApplicationCommandType.Message): InteractionRoutes;
429
460
  //#endregion
430
- //#region src/interfaces/CoreBase.d.ts
431
- /**
432
- * The transport-agnostic slice of the running framework, what core code (gates, notices, plugins'
433
- * agnostic surface) accesses through `ctx.core`. Each transport's `Core` extends it, the gateway
434
- * adds `bot` and `bus`.
435
- */
436
- interface CoreBase {
437
- /** Transports narrow this to their own config type. */
438
- readonly config: Config;
439
- /** Sliding-window rate limiting, the `Cooldown` gate's backend and a public direct-use API. */
440
- readonly rateLimiter: IRateLimiter;
441
- }
442
- //#endregion
443
- //#region src/reply/AckTrace.d.ts
444
- /** The first ack's stack, passed as the throw cause so a double-ack points to both lines. */
445
- declare class AckTrace extends Error {
446
- constructor(method: ReplyMethod);
447
- }
448
- //#endregion
449
- //#region src/reply/ackLegality.d.ts
450
- type AckState = 'unacked' | 'deferred-reply' | 'deferred-update' | 'replied';
451
- type ReplyMethod = 'reply' | 'defer' | 'deferUpdate' | 'update' | 'followUp' | 'edit' | 'delete' | 'send' | 'showModal';
452
- /** Throws before any API call. `ackedBy` becomes the throw cause, its stack points at the first ack. */
453
- declare function checkAckLegality(method: ReplyMethod, state: AckState, routeId: string, ackedBy?: AckTrace): void;
454
- declare function sendTarget(state: AckState): 'reply' | 'edit' | 'followUp';
455
- //#endregion
456
461
  //#region src/reply/serializeReply.d.ts
457
462
  /** Serialized message data the transport wire writers consume. */
458
463
  interface SerializedReply {
@@ -478,10 +483,13 @@ declare abstract class BaseReplySender<TMessage extends {
478
483
  id: string;
479
484
  }> {
480
485
  protected readonly routeId: string;
486
+ private readonly telemetry?;
481
487
  private state;
482
488
  private readonly sent;
483
489
  private ackedBy?;
484
- protected constructor(routeId: string, initialState?: AckState);
490
+ protected constructor(routeId: string, initialState?: AckState, telemetry?: ReplyTelemetry | undefined);
491
+ private report;
492
+ private attempt;
485
493
  reply(response: ReplyResponse | string, opts?: SendOpts): Promise<TMessage>;
486
494
  defer(opts?: DeferOpts): Promise<void>;
487
495
  deferUpdate(): Promise<void>;
@@ -729,72 +737,6 @@ declare function defineEffectGate<const Name extends string, Ctx extends GateCon
729
737
  */
730
738
  type RequiredOf<TGate> = TGate extends Gate<infer Ctx> ? Ctx : never;
731
739
  //#endregion
732
- //#region src/stops/Notice.d.ts
733
- /**
734
- * Base class for a user-facing refusal or a reported fault.
735
- *
736
- * Throw a `Notice` to stop a handler and reply to the user. The framework catches it at the controller
737
- * boundary and renders {@link Notice.render}, which always decides what the user sees. With `report`
738
- * false that render is all that happens. With `report` true the framework also logs the fault and
739
- * publishes it to the `handledException` bus. A raw, non-Notice throw shows the generic message.
740
- *
741
- * @example
742
- * ```ts
743
- * import { Notice, BuilderComponent, type RenderContext, type ReplyResponse } from '@seedcord/gateway';
744
- * import { TextDisplayBuilder } from '@discordjs/builders';
745
- *
746
- * // reading `.component` applies the configured bot color to the container accent
747
- * class TooPoorCard extends BuilderComponent<'container'> {
748
- * constructor(balance: number) {
749
- * super('container');
750
- * this.instance.addTextDisplayComponents(
751
- * new TextDisplayBuilder().setContent(`### Insufficient balance\nYou need more than ${balance} coins.`)
752
- * );
753
- * }
754
- * }
755
- *
756
- * class TooPoor extends Notice {
757
- * constructor(private readonly balance: number) {
758
- * super(`balance ${balance} is below the cost`);
759
- * }
760
- *
761
- * render(_ctx: RenderContext): ReplyResponse {
762
- * return { components: [new TooPoorCard(this.balance).component] };
763
- * }
764
- * }
765
- *
766
- * // in a handler, throwing stops the handler and replies with render(ctx)
767
- * if (wallet.balance < cost) throw new TooPoor(wallet.balance);
768
- * ```
769
- */
770
- declare abstract class Notice extends Error {
771
- /**
772
- * Whether this denial is a reported fault. True also logs it and publishes it to the `handledException`
773
- * bus. The user always sees {@link Notice.render} either way.
774
- *
775
- * @defaultValue `false`
776
- */
777
- report: boolean;
778
- /**
779
- * Whether the reply is ephemeral, so only the invoking user sees it. Set it false for a refusal the
780
- * whole channel should see.
781
- *
782
- * @defaultValue `true`
783
- */
784
- ephemeral: boolean;
785
- /**
786
- * A short one-line reason. When every arm of an `or` gate refuses and each refusal sets this, `or`
787
- * lists them instead of showing a neutral message.
788
- */
789
- summary?: string;
790
- protected constructor(message: string, options?: ErrorOptions);
791
- /**
792
- * Builds what the user sees. Called fresh each time the denial is shown, so the builders are new
793
- * and the bot color resolves at render time rather than at construction time.
794
- */
795
- abstract render(ctx: RenderContext): ReplyResponse;
796
- }
797
- //#endregion
798
740
  //#region src/gates/matching.d.ts
799
741
  type IntersectRequired<Gates extends readonly Gate<GateContextBase>[]> = Gates extends readonly [infer First extends Gate<GateContextBase>, ...infer Rest extends readonly Gate<GateContextBase>[]] ? RequiredOf<First> & IntersectRequired<Rest> : unknown;
800
742
  type TwoOrMore<Item> = readonly [Item, Item, ...Item[]];
@@ -996,5 +938,5 @@ type Getter<Route extends keyof SlashOptionRegistry, Kind extends OptionKind, Me
996
938
  */
997
939
  type AutocompleteOptions<Route extends keyof SlashOptionRegistry> = Getter<Route, 'string', 'getString'> & Getter<Route, 'integer', 'getInteger'> & Getter<Route, 'number', 'getNumber'> & Getter<Route, 'boolean', 'getBoolean'>;
998
940
  //#endregion
999
- export { InteractionMetadataKey as $, serializeReply as A, contextMenuRouteOf as B, GuildPermissionsContext as C, BaseReplySender as D, defineGate as E, AckTrace as F, CustomId as G, storeComponentRoute as H, CoreBase as I, CustomIdField as J, decodeFor as K, RoutableConstructor as L, ReplyMethod as M, checkAckLegality as N, ModalLike as O, sendTarget as P, GatedMetadataKey as Q, SelectMenuKind as R, GateContextBase as S, defineEffectGate as T, storeInteractionRoute as U, selectMenuRouteOf as V, AnyCustomId as W, CommandMetadataKey as X, DecodedParams as Y, EventMetadataKey as Z, JoinNames as _, FocusedField as a, CommandMeta as at, EffectGate as b, RequireBotPermissions as c, BuilderComponent as ct, RequireRole as d, RowType as dt, InteractionRouteKeys as et, RequireRoleOptions as f, OptionKind as ft, IntersectRequired as g, GateFitsWith as h, EntryFor as i, WebhookUrlMetadataKey as it, AckState as j, SerializedReply as k, RequirePermissions as l, RowComponent as lt, pickNotice as m, SlashOptionRegistry as mt, AutocompleteOptions as n, MiddlewareMetadataKey as nt, PermissionNames as o, RegisterCommand as ot, GateNoticeOptions as p, SlashOption as pt, prefixOf as q, ChoiceValueOf as r, SubscribeMetadataKey as rt, PermissionScope as s, Constructor as st, AutocompletableNames as t, InteractionRoutes as tt, RequirePermissionsOptions as u, BuilderType as ut, TwoOrMore as v, RequiredOf as w, Gate as x, Notice as y, areRoutes as z };
1000
- //# sourceMappingURL=AutocompleteOptions-eB87SZ4a.d.mts.map
941
+ export { SlashOptionRegistry as $, RoutableConstructor as A, decodeFor as B, RequiredOf as C, ModalLike as D, BaseReplySender as E, selectMenuRouteOf as F, RegisterCommand as G, CustomIdField as H, storeComponentRoute as I, RowComponent as J, IsNever as K, storeInteractionRoute as L, areRoutes as M, contextMenuRouteOf as N, SerializedReply as O, interactionRoutesOf as P, SlashOption as Q, AnyCustomId as R, GuildPermissionsContext as S, defineGate as T, DecodedParams as U, prefixOf as V, CommandMeta as W, RowType as X, BuilderType as Y, OptionKind as Z, JoinNames as _, FocusedField as a, Gate as b, RequireBotPermissions as c, RequireRole as d, RequireRoleOptions as f, IntersectRequired as g, GateFitsWith as h, EntryFor as i, SelectMenuKind as j, serializeReply as k, RequirePermissions as l, pickNotice as m, AutocompleteOptions as n, PermissionNames as o, GateNoticeOptions as p, BuilderComponent as q, ChoiceValueOf as r, PermissionScope as s, AutocompletableNames as t, RequirePermissionsOptions as u, TwoOrMore as v, defineEffectGate as w, GateContextBase as x, EffectGate as y, CustomId as z };
942
+ //# sourceMappingURL=AutocompleteOptions-DLp_B8as.d.mts.map
@@ -0,0 +1,163 @@
1
+ import { formatFilePath } from "@seedcord/utils";
2
+ import chalk from "chalk";
3
+ import { existsSync } from "node:fs";
4
+ import { resolve, sep } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ //#region src/hmr/HmrModuleHandler.ts
8
+ const displayName = (unit, fallback) => unit.name ?? fallback;
9
+ /**
10
+ * Reloads THandler and optional TMiddleware modules on HMR updates.
11
+ *
12
+ * Call `handle()` from your `HmrAware.onHmr()` for standard reload behavior, or write a fully custom `onHmr()` and skip this class.
13
+ *
14
+ * This is only useful during development.
15
+ */
16
+ var HmrModuleHandler = class {
17
+ store;
18
+ options;
19
+ constructor(options) {
20
+ this.options = {
21
+ ...options,
22
+ logger: options.logger.inChannel("hmr")
23
+ };
24
+ this.store = {
25
+ fileToHandlers: /* @__PURE__ */ new Map(),
26
+ fileToMiddlewares: /* @__PURE__ */ new Map(),
27
+ handlerArtifacts: /* @__PURE__ */ new Map()
28
+ };
29
+ }
30
+ /** Unloads or reloads the modules affected by an {@link HmrUpdateEvent}, scoped to the handler and middleware directories. */
31
+ async handle(event) {
32
+ const { file, affectedModules, type } = event;
33
+ const { logger, handlersDir, middlewaresDir } = this.options;
34
+ if (type === "delete" || type === "deleteDir") {
35
+ this.unload(file);
36
+ return;
37
+ }
38
+ if (type === "createDir") return;
39
+ if (type === "update" && !existsSync(file)) {
40
+ if (this.isTracked(file)) {
41
+ logger.info(`Received update for non-existent file: ${formatFilePath(file)}, treating as delete`);
42
+ this.unload(file);
43
+ }
44
+ return;
45
+ }
46
+ const rollbackEnabled = event.rollback ?? true;
47
+ const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];
48
+ const absHandlersDir = resolve(process.cwd(), handlersDir);
49
+ const absMiddlewaresDir = middlewaresDir ? resolve(process.cwd(), middlewaresDir) : null;
50
+ for (const fileToReload of filesToReload) {
51
+ const isHandler = fileToReload.startsWith(absHandlersDir + sep);
52
+ const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir + sep) : false;
53
+ if (!isHandler && !isMiddleware) continue;
54
+ await this.reloadWithRollback(fileToReload, rollbackEnabled);
55
+ }
56
+ }
57
+ async reloadWithRollback(file, rollbackEnabled) {
58
+ const { logger } = this.options;
59
+ const snapshot = this.snapshotUnits(file);
60
+ this.unload(file);
61
+ if (await this.reloadFile(file)) return;
62
+ this.unload(file);
63
+ if (!rollbackEnabled) return;
64
+ if (snapshot.handlers.length === 0 && snapshot.middlewares.length === 0) return;
65
+ this.restoreUnits(file, snapshot);
66
+ logger.warn(`${chalk.yellow.bold("Rolled back")} ${chalk.gray(formatFilePath(file))} to the last-good version`);
67
+ }
68
+ /** Links the handler to its source file so a later update can unload it. */
69
+ trackHandler(file, handler) {
70
+ let set = this.store.fileToHandlers.get(file);
71
+ if (!set) {
72
+ set = /* @__PURE__ */ new Set();
73
+ this.store.fileToHandlers.set(file, set);
74
+ }
75
+ set.add(handler);
76
+ if (this.options.getArtifacts) {
77
+ const artifacts = this.options.getArtifacts(handler);
78
+ this.store.handlerArtifacts.set(handler, artifacts);
79
+ }
80
+ }
81
+ /** Links the middleware to its source file so a later update can unload it. */
82
+ trackMiddleware(file, middleware) {
83
+ let set = this.store.fileToMiddlewares.get(file);
84
+ if (!set) {
85
+ set = /* @__PURE__ */ new Set();
86
+ this.store.fileToMiddlewares.set(file, set);
87
+ }
88
+ set.add(middleware);
89
+ }
90
+ isTracked(file) {
91
+ return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);
92
+ }
93
+ unload(file) {
94
+ const { logger } = this.options;
95
+ const handlers = this.store.fileToHandlers.get(file);
96
+ if (handlers) {
97
+ for (const handler of handlers) {
98
+ const artifacts = this.store.handlerArtifacts.get(handler);
99
+ this.options.unregisterHandler(handler, artifacts);
100
+ this.store.handlerArtifacts.delete(handler);
101
+ const name = displayName(handler, "Handler");
102
+ logger.info(`${chalk.red.bold("Unloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
103
+ }
104
+ this.store.fileToHandlers.delete(file);
105
+ }
106
+ const middlewares = this.store.fileToMiddlewares.get(file);
107
+ if (middlewares) {
108
+ for (const middleware of middlewares) {
109
+ this.options.unregisterMiddleware?.(middleware);
110
+ const name = displayName(middleware, "Middleware");
111
+ logger.info(`${chalk.red.bold("Unloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
112
+ }
113
+ this.store.fileToMiddlewares.delete(file);
114
+ }
115
+ }
116
+ async reloadFile(file) {
117
+ const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;
118
+ if (!existsSync(file)) {
119
+ logger.info(`File does not exist, skipping reload: ${formatFilePath(file)}`);
120
+ return false;
121
+ }
122
+ try {
123
+ let fileUrl = pathToFileURL(file).href;
124
+ if (process.env.VITEST === "true") fileUrl += `?update=${Date.now()}`;
125
+ const imported = await import(fileUrl);
126
+ for (const val of Object.values(imported)) if (isHandler(val)) {
127
+ registerHandler(val, file);
128
+ this.trackHandler(file, val);
129
+ const name = displayName(val, "Handler");
130
+ logger.info(`${chalk.blue.bold("Reloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
131
+ } else if (isMiddleware && registerMiddleware && isMiddleware(val)) {
132
+ registerMiddleware(val, file);
133
+ this.trackMiddleware(file, val);
134
+ const name = displayName(val, "Middleware");
135
+ logger.info(`${chalk.blue.bold("Reloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
136
+ }
137
+ return true;
138
+ } catch (error) {
139
+ logger.error(`Failed to reload file: ${file}`, error);
140
+ return false;
141
+ }
142
+ }
143
+ snapshotUnits(file) {
144
+ return {
145
+ handlers: [...this.store.fileToHandlers.get(file) ?? []],
146
+ middlewares: [...this.store.fileToMiddlewares.get(file) ?? []]
147
+ };
148
+ }
149
+ restoreUnits(file, snapshot) {
150
+ for (const handler of snapshot.handlers) {
151
+ this.options.registerHandler(handler, file);
152
+ this.trackHandler(file, handler);
153
+ }
154
+ if (this.options.registerMiddleware) for (const middleware of snapshot.middlewares) {
155
+ this.options.registerMiddleware(middleware, file);
156
+ this.trackMiddleware(file, middleware);
157
+ }
158
+ }
159
+ };
160
+
161
+ //#endregion
162
+ export { HmrModuleHandler as t };
163
+ //# sourceMappingURL=HmrModuleHandler-3sNgKHdf.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HmrModuleHandler-3sNgKHdf.mjs","names":[],"sources":["../src/hmr/HmrModuleHandler.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { resolve, sep } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { formatFilePath } from '@seedcord/utils';\nimport chalk from 'chalk';\n\nimport type { Logger } from '@seedcord/logger';\nimport type { HmrUpdateEvent } from '@seedcord/types';\n\ninterface HmrStore<THandler, TMiddleware, TArtifacts> {\n fileToHandlers: Map<string, Set<THandler>>;\n fileToMiddlewares: Map<string, Set<TMiddleware>>;\n handlerArtifacts: Map<THandler, TArtifacts>;\n}\n\n// justified: units are caller-supplied classes, name is read for log labels only\nconst displayName = (unit: unknown, fallback: string): string => (unit as { name?: string }).name ?? fallback;\n\ninterface HmrModuleHandlerOptions<THandler, TMiddleware = void, TArtifacts = unknown> {\n handlersDir: string;\n middlewaresDir?: string;\n isHandler: (val: unknown) => val is THandler;\n isMiddleware?: (val: unknown) => val is TMiddleware;\n registerHandler: (handler: THandler, file: string) => void;\n registerMiddleware?: (middleware: TMiddleware, file: string) => void;\n unregisterHandler: (handler: THandler, artifacts?: TArtifacts) => void;\n unregisterMiddleware?: (middleware: TMiddleware) => void;\n /** Snapshotted per handler so a rollback re-registers with the same artifacts after the module re-imports. */\n getArtifacts?: (handler: THandler) => TArtifacts;\n logger: Logger;\n}\n\n/**\n * Reloads THandler and optional TMiddleware modules on HMR updates.\n *\n * Call `handle()` from your `HmrAware.onHmr()` for standard reload behavior, or write a fully custom `onHmr()` and skip this class.\n *\n * This is only useful during development.\n */\nexport class HmrModuleHandler<THandler, TMiddleware = void, TArtifacts = unknown> {\n private readonly store: HmrStore<THandler, TMiddleware, TArtifacts>;\n private readonly options: HmrModuleHandlerOptions<THandler, TMiddleware, TArtifacts>;\n\n constructor(options: HmrModuleHandlerOptions<THandler, TMiddleware, TArtifacts>) {\n // the caller's options object stays unmutated\n this.options = { ...options, logger: options.logger.inChannel('hmr') };\n\n // no import.meta.hot.data stash needed, a leaf reload never reconstructs the singleton that holds these Maps\n this.store = {\n fileToHandlers: new Map(),\n fileToMiddlewares: new Map(),\n handlerArtifacts: new Map()\n };\n }\n\n /** Unloads or reloads the modules affected by an {@link HmrUpdateEvent}, scoped to the handler and middleware directories. */\n public async handle(event: HmrUpdateEvent): Promise<void> {\n const { file, affectedModules, type } = event;\n const { logger, handlersDir, middlewaresDir } = this.options;\n\n if (type === 'delete' || type === 'deleteDir') {\n this.unload(file);\n return;\n }\n\n // a directory is never importable, its files arrive as their own create events\n if (type === 'createDir') return;\n\n if (type === 'update' && !existsSync(file)) {\n if (this.isTracked(file)) {\n logger.info(`Received update for non-existent file: ${formatFilePath(file)}, treating as delete`);\n this.unload(file);\n }\n return;\n }\n\n const rollbackEnabled = event.rollback ?? true;\n const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];\n const absHandlersDir = resolve(process.cwd(), handlersDir);\n const absMiddlewaresDir = middlewaresDir ? resolve(process.cwd(), middlewaresDir) : null;\n\n for (const fileToReload of filesToReload) {\n // the separator keeps a sibling dir sharing the prefix (commands-extra vs commands) out\n const isHandler = fileToReload.startsWith(absHandlersDir + sep);\n const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir + sep) : false;\n\n if (!isHandler && !isMiddleware) {\n continue;\n }\n\n await this.reloadWithRollback(fileToReload, rollbackEnabled);\n }\n }\n\n private async reloadWithRollback(file: string, rollbackEnabled: boolean): Promise<void> {\n const { logger } = this.options;\n const snapshot = this.snapshotUnits(file);\n this.unload(file);\n\n const reloaded = await this.reloadFile(file);\n if (reloaded) return;\n\n // a failed reload can leave a partial registration\n this.unload(file);\n if (!rollbackEnabled) return;\n if (snapshot.handlers.length === 0 && snapshot.middlewares.length === 0) return;\n\n // a typo keeps them live until the next good save\n this.restoreUnits(file, snapshot);\n logger.warn(`${chalk.yellow.bold('Rolled back')} ${chalk.gray(formatFilePath(file))} to the last-good version`);\n }\n\n /** Links the handler to its source file so a later update can unload it. */\n public trackHandler(file: string, handler: THandler): void {\n let set = this.store.fileToHandlers.get(file);\n if (!set) {\n set = new Set();\n this.store.fileToHandlers.set(file, set);\n }\n set.add(handler);\n\n if (this.options.getArtifacts) {\n const artifacts = this.options.getArtifacts(handler);\n this.store.handlerArtifacts.set(handler, artifacts);\n }\n }\n\n /** Links the middleware to its source file so a later update can unload it. */\n public trackMiddleware(file: string, middleware: TMiddleware): void {\n let set = this.store.fileToMiddlewares.get(file);\n if (!set) {\n set = new Set();\n this.store.fileToMiddlewares.set(file, set);\n }\n set.add(middleware);\n }\n\n private isTracked(file: string): boolean {\n return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);\n }\n\n private unload(file: string): void {\n const { logger } = this.options;\n const handlers = this.store.fileToHandlers.get(file);\n if (handlers) {\n for (const handler of handlers) {\n const artifacts = this.store.handlerArtifacts.get(handler);\n this.options.unregisterHandler(handler, artifacts);\n this.store.handlerArtifacts.delete(handler);\n const name = displayName(handler, 'Handler');\n logger.info(\n `${chalk.red.bold('Unloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n }\n this.store.fileToHandlers.delete(file);\n }\n\n const middlewares = this.store.fileToMiddlewares.get(file);\n if (middlewares) {\n for (const middleware of middlewares) {\n this.options.unregisterMiddleware?.(middleware);\n const name = displayName(middleware, 'Middleware');\n logger.info(\n `${chalk.red.bold('Unloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n }\n this.store.fileToMiddlewares.delete(file);\n }\n }\n\n private async reloadFile(file: string): Promise<boolean> {\n const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;\n\n if (!existsSync(file)) {\n logger.info(`File does not exist, skipping reload: ${formatFilePath(file)}`);\n return false;\n }\n\n try {\n let fileUrl = pathToFileURL(file).href;\n // vitest has no real vite server managing the module graph, so bust the import cache manually.\n if (process.env.VITEST === 'true') fileUrl += `?update=${Date.now()}`;\n\n // justified: a dynamic import resolves to an untyped export map\n const imported = (await import(fileUrl)) as Record<string, unknown>;\n\n for (const val of Object.values(imported)) {\n if (isHandler(val)) {\n registerHandler(val, file);\n this.trackHandler(file, val);\n const name = displayName(val, 'Handler');\n logger.info(\n `${chalk.blue.bold('Reloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n } else if (isMiddleware && registerMiddleware && isMiddleware(val)) {\n registerMiddleware(val, file);\n this.trackMiddleware(file, val);\n const name = displayName(val, 'Middleware');\n logger.info(\n `${chalk.blue.bold('Reloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n }\n }\n\n return true;\n } catch (error) {\n logger.error(`Failed to reload file: ${file}`, error);\n return false;\n }\n }\n\n // restoring re-registers the same class objects, so a live db connection survives rollback\n private snapshotUnits(file: string): { handlers: THandler[]; middlewares: TMiddleware[] } {\n return {\n handlers: [...(this.store.fileToHandlers.get(file) ?? [])],\n middlewares: [...(this.store.fileToMiddlewares.get(file) ?? [])]\n };\n }\n\n private restoreUnits(file: string, snapshot: { handlers: THandler[]; middlewares: TMiddleware[] }): void {\n for (const handler of snapshot.handlers) {\n this.options.registerHandler(handler, file);\n this.trackHandler(file, handler);\n }\n\n if (this.options.registerMiddleware) {\n for (const middleware of snapshot.middlewares) {\n this.options.registerMiddleware(middleware, file);\n this.trackMiddleware(file, middleware);\n }\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,170 @@
1
+ const require_rolldown_runtime = require('./rolldown-runtime-C_NdSu1c.cjs');
2
+ let _seedcord_utils = require("@seedcord/utils");
3
+ let chalk = require("chalk");
4
+ chalk = require_rolldown_runtime.__toESM(chalk, 1);
5
+ let node_fs = require("node:fs");
6
+ let node_path = require("node:path");
7
+ let node_url = require("node:url");
8
+
9
+ //#region src/hmr/HmrModuleHandler.ts
10
+ const displayName = (unit, fallback) => unit.name ?? fallback;
11
+ /**
12
+ * Reloads THandler and optional TMiddleware modules on HMR updates.
13
+ *
14
+ * Call `handle()` from your `HmrAware.onHmr()` for standard reload behavior, or write a fully custom `onHmr()` and skip this class.
15
+ *
16
+ * This is only useful during development.
17
+ */
18
+ var HmrModuleHandler = class {
19
+ store;
20
+ options;
21
+ constructor(options) {
22
+ this.options = {
23
+ ...options,
24
+ logger: options.logger.inChannel("hmr")
25
+ };
26
+ this.store = {
27
+ fileToHandlers: /* @__PURE__ */ new Map(),
28
+ fileToMiddlewares: /* @__PURE__ */ new Map(),
29
+ handlerArtifacts: /* @__PURE__ */ new Map()
30
+ };
31
+ }
32
+ /** Unloads or reloads the modules affected by an {@link HmrUpdateEvent}, scoped to the handler and middleware directories. */
33
+ async handle(event) {
34
+ const { file, affectedModules, type } = event;
35
+ const { logger, handlersDir, middlewaresDir } = this.options;
36
+ if (type === "delete" || type === "deleteDir") {
37
+ this.unload(file);
38
+ return;
39
+ }
40
+ if (type === "createDir") return;
41
+ if (type === "update" && !(0, node_fs.existsSync)(file)) {
42
+ if (this.isTracked(file)) {
43
+ logger.info(`Received update for non-existent file: ${(0, _seedcord_utils.formatFilePath)(file)}, treating as delete`);
44
+ this.unload(file);
45
+ }
46
+ return;
47
+ }
48
+ const rollbackEnabled = event.rollback ?? true;
49
+ const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];
50
+ const absHandlersDir = (0, node_path.resolve)(process.cwd(), handlersDir);
51
+ const absMiddlewaresDir = middlewaresDir ? (0, node_path.resolve)(process.cwd(), middlewaresDir) : null;
52
+ for (const fileToReload of filesToReload) {
53
+ const isHandler = fileToReload.startsWith(absHandlersDir + node_path.sep);
54
+ const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir + node_path.sep) : false;
55
+ if (!isHandler && !isMiddleware) continue;
56
+ await this.reloadWithRollback(fileToReload, rollbackEnabled);
57
+ }
58
+ }
59
+ async reloadWithRollback(file, rollbackEnabled) {
60
+ const { logger } = this.options;
61
+ const snapshot = this.snapshotUnits(file);
62
+ this.unload(file);
63
+ if (await this.reloadFile(file)) return;
64
+ this.unload(file);
65
+ if (!rollbackEnabled) return;
66
+ if (snapshot.handlers.length === 0 && snapshot.middlewares.length === 0) return;
67
+ this.restoreUnits(file, snapshot);
68
+ logger.warn(`${chalk.default.yellow.bold("Rolled back")} ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))} to the last-good version`);
69
+ }
70
+ /** Links the handler to its source file so a later update can unload it. */
71
+ trackHandler(file, handler) {
72
+ let set = this.store.fileToHandlers.get(file);
73
+ if (!set) {
74
+ set = /* @__PURE__ */ new Set();
75
+ this.store.fileToHandlers.set(file, set);
76
+ }
77
+ set.add(handler);
78
+ if (this.options.getArtifacts) {
79
+ const artifacts = this.options.getArtifacts(handler);
80
+ this.store.handlerArtifacts.set(handler, artifacts);
81
+ }
82
+ }
83
+ /** Links the middleware to its source file so a later update can unload it. */
84
+ trackMiddleware(file, middleware) {
85
+ let set = this.store.fileToMiddlewares.get(file);
86
+ if (!set) {
87
+ set = /* @__PURE__ */ new Set();
88
+ this.store.fileToMiddlewares.set(file, set);
89
+ }
90
+ set.add(middleware);
91
+ }
92
+ isTracked(file) {
93
+ return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);
94
+ }
95
+ unload(file) {
96
+ const { logger } = this.options;
97
+ const handlers = this.store.fileToHandlers.get(file);
98
+ if (handlers) {
99
+ for (const handler of handlers) {
100
+ const artifacts = this.store.handlerArtifacts.get(handler);
101
+ this.options.unregisterHandler(handler, artifacts);
102
+ this.store.handlerArtifacts.delete(handler);
103
+ const name = displayName(handler, "Handler");
104
+ logger.info(`${chalk.default.red.bold("Unloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
105
+ }
106
+ this.store.fileToHandlers.delete(file);
107
+ }
108
+ const middlewares = this.store.fileToMiddlewares.get(file);
109
+ if (middlewares) {
110
+ for (const middleware of middlewares) {
111
+ this.options.unregisterMiddleware?.(middleware);
112
+ const name = displayName(middleware, "Middleware");
113
+ logger.info(`${chalk.default.red.bold("Unloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
114
+ }
115
+ this.store.fileToMiddlewares.delete(file);
116
+ }
117
+ }
118
+ async reloadFile(file) {
119
+ const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;
120
+ if (!(0, node_fs.existsSync)(file)) {
121
+ logger.info(`File does not exist, skipping reload: ${(0, _seedcord_utils.formatFilePath)(file)}`);
122
+ return false;
123
+ }
124
+ try {
125
+ let fileUrl = (0, node_url.pathToFileURL)(file).href;
126
+ if (process.env.VITEST === "true") fileUrl += `?update=${Date.now()}`;
127
+ const imported = await import(fileUrl);
128
+ for (const val of Object.values(imported)) if (isHandler(val)) {
129
+ registerHandler(val, file);
130
+ this.trackHandler(file, val);
131
+ const name = displayName(val, "Handler");
132
+ logger.info(`${chalk.default.blue.bold("Reloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
133
+ } else if (isMiddleware && registerMiddleware && isMiddleware(val)) {
134
+ registerMiddleware(val, file);
135
+ this.trackMiddleware(file, val);
136
+ const name = displayName(val, "Middleware");
137
+ logger.info(`${chalk.default.blue.bold("Reloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
138
+ }
139
+ return true;
140
+ } catch (error) {
141
+ logger.error(`Failed to reload file: ${file}`, error);
142
+ return false;
143
+ }
144
+ }
145
+ snapshotUnits(file) {
146
+ return {
147
+ handlers: [...this.store.fileToHandlers.get(file) ?? []],
148
+ middlewares: [...this.store.fileToMiddlewares.get(file) ?? []]
149
+ };
150
+ }
151
+ restoreUnits(file, snapshot) {
152
+ for (const handler of snapshot.handlers) {
153
+ this.options.registerHandler(handler, file);
154
+ this.trackHandler(file, handler);
155
+ }
156
+ if (this.options.registerMiddleware) for (const middleware of snapshot.middlewares) {
157
+ this.options.registerMiddleware(middleware, file);
158
+ this.trackMiddleware(file, middleware);
159
+ }
160
+ }
161
+ };
162
+
163
+ //#endregion
164
+ Object.defineProperty(exports, 'HmrModuleHandler', {
165
+ enumerable: true,
166
+ get: function () {
167
+ return HmrModuleHandler;
168
+ }
169
+ });
170
+ //# sourceMappingURL=HmrModuleHandler-CTBng7wV.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HmrModuleHandler-CTBng7wV.cjs","names":["sep"],"sources":["../src/hmr/HmrModuleHandler.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { resolve, sep } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { formatFilePath } from '@seedcord/utils';\nimport chalk from 'chalk';\n\nimport type { Logger } from '@seedcord/logger';\nimport type { HmrUpdateEvent } from '@seedcord/types';\n\ninterface HmrStore<THandler, TMiddleware, TArtifacts> {\n fileToHandlers: Map<string, Set<THandler>>;\n fileToMiddlewares: Map<string, Set<TMiddleware>>;\n handlerArtifacts: Map<THandler, TArtifacts>;\n}\n\n// justified: units are caller-supplied classes, name is read for log labels only\nconst displayName = (unit: unknown, fallback: string): string => (unit as { name?: string }).name ?? fallback;\n\ninterface HmrModuleHandlerOptions<THandler, TMiddleware = void, TArtifacts = unknown> {\n handlersDir: string;\n middlewaresDir?: string;\n isHandler: (val: unknown) => val is THandler;\n isMiddleware?: (val: unknown) => val is TMiddleware;\n registerHandler: (handler: THandler, file: string) => void;\n registerMiddleware?: (middleware: TMiddleware, file: string) => void;\n unregisterHandler: (handler: THandler, artifacts?: TArtifacts) => void;\n unregisterMiddleware?: (middleware: TMiddleware) => void;\n /** Snapshotted per handler so a rollback re-registers with the same artifacts after the module re-imports. */\n getArtifacts?: (handler: THandler) => TArtifacts;\n logger: Logger;\n}\n\n/**\n * Reloads THandler and optional TMiddleware modules on HMR updates.\n *\n * Call `handle()` from your `HmrAware.onHmr()` for standard reload behavior, or write a fully custom `onHmr()` and skip this class.\n *\n * This is only useful during development.\n */\nexport class HmrModuleHandler<THandler, TMiddleware = void, TArtifacts = unknown> {\n private readonly store: HmrStore<THandler, TMiddleware, TArtifacts>;\n private readonly options: HmrModuleHandlerOptions<THandler, TMiddleware, TArtifacts>;\n\n constructor(options: HmrModuleHandlerOptions<THandler, TMiddleware, TArtifacts>) {\n // the caller's options object stays unmutated\n this.options = { ...options, logger: options.logger.inChannel('hmr') };\n\n // no import.meta.hot.data stash needed, a leaf reload never reconstructs the singleton that holds these Maps\n this.store = {\n fileToHandlers: new Map(),\n fileToMiddlewares: new Map(),\n handlerArtifacts: new Map()\n };\n }\n\n /** Unloads or reloads the modules affected by an {@link HmrUpdateEvent}, scoped to the handler and middleware directories. */\n public async handle(event: HmrUpdateEvent): Promise<void> {\n const { file, affectedModules, type } = event;\n const { logger, handlersDir, middlewaresDir } = this.options;\n\n if (type === 'delete' || type === 'deleteDir') {\n this.unload(file);\n return;\n }\n\n // a directory is never importable, its files arrive as their own create events\n if (type === 'createDir') return;\n\n if (type === 'update' && !existsSync(file)) {\n if (this.isTracked(file)) {\n logger.info(`Received update for non-existent file: ${formatFilePath(file)}, treating as delete`);\n this.unload(file);\n }\n return;\n }\n\n const rollbackEnabled = event.rollback ?? true;\n const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];\n const absHandlersDir = resolve(process.cwd(), handlersDir);\n const absMiddlewaresDir = middlewaresDir ? resolve(process.cwd(), middlewaresDir) : null;\n\n for (const fileToReload of filesToReload) {\n // the separator keeps a sibling dir sharing the prefix (commands-extra vs commands) out\n const isHandler = fileToReload.startsWith(absHandlersDir + sep);\n const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir + sep) : false;\n\n if (!isHandler && !isMiddleware) {\n continue;\n }\n\n await this.reloadWithRollback(fileToReload, rollbackEnabled);\n }\n }\n\n private async reloadWithRollback(file: string, rollbackEnabled: boolean): Promise<void> {\n const { logger } = this.options;\n const snapshot = this.snapshotUnits(file);\n this.unload(file);\n\n const reloaded = await this.reloadFile(file);\n if (reloaded) return;\n\n // a failed reload can leave a partial registration\n this.unload(file);\n if (!rollbackEnabled) return;\n if (snapshot.handlers.length === 0 && snapshot.middlewares.length === 0) return;\n\n // a typo keeps them live until the next good save\n this.restoreUnits(file, snapshot);\n logger.warn(`${chalk.yellow.bold('Rolled back')} ${chalk.gray(formatFilePath(file))} to the last-good version`);\n }\n\n /** Links the handler to its source file so a later update can unload it. */\n public trackHandler(file: string, handler: THandler): void {\n let set = this.store.fileToHandlers.get(file);\n if (!set) {\n set = new Set();\n this.store.fileToHandlers.set(file, set);\n }\n set.add(handler);\n\n if (this.options.getArtifacts) {\n const artifacts = this.options.getArtifacts(handler);\n this.store.handlerArtifacts.set(handler, artifacts);\n }\n }\n\n /** Links the middleware to its source file so a later update can unload it. */\n public trackMiddleware(file: string, middleware: TMiddleware): void {\n let set = this.store.fileToMiddlewares.get(file);\n if (!set) {\n set = new Set();\n this.store.fileToMiddlewares.set(file, set);\n }\n set.add(middleware);\n }\n\n private isTracked(file: string): boolean {\n return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);\n }\n\n private unload(file: string): void {\n const { logger } = this.options;\n const handlers = this.store.fileToHandlers.get(file);\n if (handlers) {\n for (const handler of handlers) {\n const artifacts = this.store.handlerArtifacts.get(handler);\n this.options.unregisterHandler(handler, artifacts);\n this.store.handlerArtifacts.delete(handler);\n const name = displayName(handler, 'Handler');\n logger.info(\n `${chalk.red.bold('Unloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n }\n this.store.fileToHandlers.delete(file);\n }\n\n const middlewares = this.store.fileToMiddlewares.get(file);\n if (middlewares) {\n for (const middleware of middlewares) {\n this.options.unregisterMiddleware?.(middleware);\n const name = displayName(middleware, 'Middleware');\n logger.info(\n `${chalk.red.bold('Unloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n }\n this.store.fileToMiddlewares.delete(file);\n }\n }\n\n private async reloadFile(file: string): Promise<boolean> {\n const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;\n\n if (!existsSync(file)) {\n logger.info(`File does not exist, skipping reload: ${formatFilePath(file)}`);\n return false;\n }\n\n try {\n let fileUrl = pathToFileURL(file).href;\n // vitest has no real vite server managing the module graph, so bust the import cache manually.\n if (process.env.VITEST === 'true') fileUrl += `?update=${Date.now()}`;\n\n // justified: a dynamic import resolves to an untyped export map\n const imported = (await import(fileUrl)) as Record<string, unknown>;\n\n for (const val of Object.values(imported)) {\n if (isHandler(val)) {\n registerHandler(val, file);\n this.trackHandler(file, val);\n const name = displayName(val, 'Handler');\n logger.info(\n `${chalk.blue.bold('Reloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n } else if (isMiddleware && registerMiddleware && isMiddleware(val)) {\n registerMiddleware(val, file);\n this.trackMiddleware(file, val);\n const name = displayName(val, 'Middleware');\n logger.info(\n `${chalk.blue.bold('Reloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n }\n }\n\n return true;\n } catch (error) {\n logger.error(`Failed to reload file: ${file}`, error);\n return false;\n }\n }\n\n // restoring re-registers the same class objects, so a live db connection survives rollback\n private snapshotUnits(file: string): { handlers: THandler[]; middlewares: TMiddleware[] } {\n return {\n handlers: [...(this.store.fileToHandlers.get(file) ?? [])],\n middlewares: [...(this.store.fileToMiddlewares.get(file) ?? [])]\n };\n }\n\n private restoreUnits(file: string, snapshot: { handlers: THandler[]; middlewares: TMiddleware[] }): void {\n for (const handler of snapshot.handlers) {\n this.options.registerHandler(handler, file);\n this.trackHandler(file, handler);\n }\n\n if (this.options.registerMiddleware) {\n for (const middleware of snapshot.middlewares) {\n this.options.registerMiddleware(middleware, file);\n this.trackMiddleware(file, middleware);\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;AAiBA,MAAM,eAAe,MAAe,aAA8B,KAA2B,QAAQ;;;;;;;;AAuBrG,IAAa,mBAAb,MAAkF;CAC9E,AAAiB;CACjB,AAAiB;CAEjB,YAAY,SAAqE;EAE7E,KAAK,UAAU;GAAE,GAAG;GAAS,QAAQ,QAAQ,OAAO,UAAU,KAAK;EAAE;EAGrE,KAAK,QAAQ;GACT,gCAAgB,IAAI,IAAI;GACxB,mCAAmB,IAAI,IAAI;GAC3B,kCAAkB,IAAI,IAAI;EAC9B;CACJ;;CAGA,MAAa,OAAO,OAAsC;EACtD,MAAM,EAAE,MAAM,iBAAiB,SAAS;EACxC,MAAM,EAAE,QAAQ,aAAa,mBAAmB,KAAK;EAErD,IAAI,SAAS,YAAY,SAAS,aAAa;GAC3C,KAAK,OAAO,IAAI;GAChB;EACJ;EAGA,IAAI,SAAS,aAAa;EAE1B,IAAI,SAAS,YAAY,yBAAY,IAAI,GAAG;GACxC,IAAI,KAAK,UAAU,IAAI,GAAG;IACtB,OAAO,KAAK,8EAAyD,IAAI,EAAE,qBAAqB;IAChG,KAAK,OAAO,IAAI;GACpB;GACA;EACJ;EAEA,MAAM,kBAAkB,MAAM,YAAY;EAC1C,MAAM,gBAAgB,mBAAmB,gBAAgB,SAAS,IAAI,kBAAkB,CAAC,IAAI;EAC7F,MAAM,wCAAyB,QAAQ,IAAI,GAAG,WAAW;EACzD,MAAM,oBAAoB,wCAAyB,QAAQ,IAAI,GAAG,cAAc,IAAI;EAEpF,KAAK,MAAM,gBAAgB,eAAe;GAEtC,MAAM,YAAY,aAAa,WAAW,iBAAiBA,aAAG;GAC9D,MAAM,eAAe,oBAAoB,aAAa,WAAW,oBAAoBA,aAAG,IAAI;GAE5F,IAAI,CAAC,aAAa,CAAC,cACf;GAGJ,MAAM,KAAK,mBAAmB,cAAc,eAAe;EAC/D;CACJ;CAEA,MAAc,mBAAmB,MAAc,iBAAyC;EACpF,MAAM,EAAE,WAAW,KAAK;EACxB,MAAM,WAAW,KAAK,cAAc,IAAI;EACxC,KAAK,OAAO,IAAI;EAGhB,IAAI,MADmB,KAAK,WAAW,IAAI,GAC7B;EAGd,KAAK,OAAO,IAAI;EAChB,IAAI,CAAC,iBAAiB;EACtB,IAAI,SAAS,SAAS,WAAW,KAAK,SAAS,YAAY,WAAW,GAAG;EAGzE,KAAK,aAAa,MAAM,QAAQ;EAChC,OAAO,KAAK,GAAG,cAAM,OAAO,KAAK,aAAa,EAAE,GAAG,cAAM,yCAAoB,IAAI,CAAC,EAAE,0BAA0B;CAClH;;CAGA,AAAO,aAAa,MAAc,SAAyB;EACvD,IAAI,MAAM,KAAK,MAAM,eAAe,IAAI,IAAI;EAC5C,IAAI,CAAC,KAAK;GACN,sBAAM,IAAI,IAAI;GACd,KAAK,MAAM,eAAe,IAAI,MAAM,GAAG;EAC3C;EACA,IAAI,IAAI,OAAO;EAEf,IAAI,KAAK,QAAQ,cAAc;GAC3B,MAAM,YAAY,KAAK,QAAQ,aAAa,OAAO;GACnD,KAAK,MAAM,iBAAiB,IAAI,SAAS,SAAS;EACtD;CACJ;;CAGA,AAAO,gBAAgB,MAAc,YAA+B;EAChE,IAAI,MAAM,KAAK,MAAM,kBAAkB,IAAI,IAAI;EAC/C,IAAI,CAAC,KAAK;GACN,sBAAM,IAAI,IAAI;GACd,KAAK,MAAM,kBAAkB,IAAI,MAAM,GAAG;EAC9C;EACA,IAAI,IAAI,UAAU;CACtB;CAEA,AAAQ,UAAU,MAAuB;EACrC,OAAO,KAAK,MAAM,eAAe,IAAI,IAAI,KAAK,KAAK,MAAM,kBAAkB,IAAI,IAAI;CACvF;CAEA,AAAQ,OAAO,MAAoB;EAC/B,MAAM,EAAE,WAAW,KAAK;EACxB,MAAM,WAAW,KAAK,MAAM,eAAe,IAAI,IAAI;EACnD,IAAI,UAAU;GACV,KAAK,MAAM,WAAW,UAAU;IAC5B,MAAM,YAAY,KAAK,MAAM,iBAAiB,IAAI,OAAO;IACzD,KAAK,QAAQ,kBAAkB,SAAS,SAAS;IACjD,KAAK,MAAM,iBAAiB,OAAO,OAAO;IAC1C,MAAM,OAAO,YAAY,SAAS,SAAS;IAC3C,OAAO,KACH,GAAG,cAAM,IAAI,KAAK,UAAU,EAAE,GAAG,cAAM,KAAK,KAAK,IAAI,EAAE,QAAQ,cAAM,yCAAoB,IAAI,CAAC,GAClG;GACJ;GACA,KAAK,MAAM,eAAe,OAAO,IAAI;EACzC;EAEA,MAAM,cAAc,KAAK,MAAM,kBAAkB,IAAI,IAAI;EACzD,IAAI,aAAa;GACb,KAAK,MAAM,cAAc,aAAa;IAClC,KAAK,QAAQ,uBAAuB,UAAU;IAC9C,MAAM,OAAO,YAAY,YAAY,YAAY;IACjD,OAAO,KACH,GAAG,cAAM,IAAI,KAAK,UAAU,EAAE,GAAG,cAAM,KAAK,KAAK,IAAI,EAAE,QAAQ,cAAM,yCAAoB,IAAI,CAAC,GAClG;GACJ;GACA,KAAK,MAAM,kBAAkB,OAAO,IAAI;EAC5C;CACJ;CAEA,MAAc,WAAW,MAAgC;EACrD,MAAM,EAAE,QAAQ,WAAW,cAAc,iBAAiB,uBAAuB,KAAK;EAEtF,IAAI,yBAAY,IAAI,GAAG;GACnB,OAAO,KAAK,6EAAwD,IAAI,GAAG;GAC3E,OAAO;EACX;EAEA,IAAI;GACA,IAAI,sCAAwB,IAAI,CAAC,CAAC;GAElC,IAAI,QAAQ,IAAI,WAAW,QAAQ,WAAW,WAAW,KAAK,IAAI;GAGlE,MAAM,WAAY,MAAM,OAAO;GAE/B,KAAK,MAAM,OAAO,OAAO,OAAO,QAAQ,GACpC,IAAI,UAAU,GAAG,GAAG;IAChB,gBAAgB,KAAK,IAAI;IACzB,KAAK,aAAa,MAAM,GAAG;IAC3B,MAAM,OAAO,YAAY,KAAK,SAAS;IACvC,OAAO,KACH,GAAG,cAAM,KAAK,KAAK,UAAU,EAAE,GAAG,cAAM,KAAK,KAAK,IAAI,EAAE,QAAQ,cAAM,yCAAoB,IAAI,CAAC,GACnG;GACJ,OAAO,IAAI,gBAAgB,sBAAsB,aAAa,GAAG,GAAG;IAChE,mBAAmB,KAAK,IAAI;IAC5B,KAAK,gBAAgB,MAAM,GAAG;IAC9B,MAAM,OAAO,YAAY,KAAK,YAAY;IAC1C,OAAO,KACH,GAAG,cAAM,KAAK,KAAK,UAAU,EAAE,GAAG,cAAM,KAAK,KAAK,IAAI,EAAE,QAAQ,cAAM,yCAAoB,IAAI,CAAC,GACnG;GACJ;GAGJ,OAAO;EACX,SAAS,OAAO;GACZ,OAAO,MAAM,0BAA0B,QAAQ,KAAK;GACpD,OAAO;EACX;CACJ;CAGA,AAAQ,cAAc,MAAoE;EACtF,OAAO;GACH,UAAU,CAAC,GAAI,KAAK,MAAM,eAAe,IAAI,IAAI,KAAK,CAAC,CAAE;GACzD,aAAa,CAAC,GAAI,KAAK,MAAM,kBAAkB,IAAI,IAAI,KAAK,CAAC,CAAE;EACnE;CACJ;CAEA,AAAQ,aAAa,MAAc,UAAsE;EACrG,KAAK,MAAM,WAAW,SAAS,UAAU;GACrC,KAAK,QAAQ,gBAAgB,SAAS,IAAI;GAC1C,KAAK,aAAa,MAAM,OAAO;EACnC;EAEA,IAAI,KAAK,QAAQ,oBACb,KAAK,MAAM,cAAc,SAAS,aAAa;GAC3C,KAAK,QAAQ,mBAAmB,YAAY,IAAI;GAChD,KAAK,gBAAgB,MAAM,UAAU;EACzC;CAER;AACJ"}