@seedcord/core 0.1.0-next.3 → 0.1.0-next.4

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.
@@ -0,0 +1,166 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_rolldown_runtime = require('./rolldown-runtime-C_NdSu1c.cjs');
3
+ let _seedcord_utils = require("@seedcord/utils");
4
+ let chalk = require("chalk");
5
+ chalk = require_rolldown_runtime.__toESM(chalk, 1);
6
+ let node_fs = require("node:fs");
7
+ let node_path = require("node:path");
8
+ let node_url = require("node:url");
9
+
10
+ //#region src/hmr/HmrModuleHandler.ts
11
+ const displayName = (unit, fallback) => unit.name ?? fallback;
12
+ /**
13
+ * Reloads THandler and optional TMiddleware modules on HMR updates.
14
+ *
15
+ * Call `handle()` from your `HmrAware.onHmr()` for standard reload behavior, or write a fully custom `onHmr()` and skip this class.
16
+ *
17
+ * This is only useful during development.
18
+ */
19
+ var HmrModuleHandler = class {
20
+ store;
21
+ options;
22
+ constructor(options) {
23
+ this.options = {
24
+ ...options,
25
+ logger: options.logger.inChannel("hmr")
26
+ };
27
+ this.store = {
28
+ fileToHandlers: /* @__PURE__ */ new Map(),
29
+ fileToMiddlewares: /* @__PURE__ */ new Map(),
30
+ handlerArtifacts: /* @__PURE__ */ new Map()
31
+ };
32
+ }
33
+ /** Unloads or reloads the modules affected by an {@link HmrUpdateEvent}, scoped to the handler and middleware directories. */
34
+ async handle(event) {
35
+ const { file, affectedModules, type } = event;
36
+ const { logger, handlersDir, middlewaresDir } = this.options;
37
+ if (type === "delete" || type === "deleteDir") {
38
+ this.unload(file);
39
+ return;
40
+ }
41
+ if (type === "createDir") return;
42
+ if (type === "update" && !(0, node_fs.existsSync)(file)) {
43
+ if (this.isTracked(file)) {
44
+ logger.info(`Received update for non-existent file: ${(0, _seedcord_utils.formatFilePath)(file)}, treating as delete`);
45
+ this.unload(file);
46
+ }
47
+ return;
48
+ }
49
+ const rollbackEnabled = event.rollback ?? true;
50
+ const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];
51
+ const absHandlersDir = (0, node_path.resolve)(process.cwd(), handlersDir);
52
+ const absMiddlewaresDir = middlewaresDir ? (0, node_path.resolve)(process.cwd(), middlewaresDir) : null;
53
+ for (const fileToReload of filesToReload) {
54
+ const isHandler = fileToReload.startsWith(absHandlersDir + node_path.sep);
55
+ const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir + node_path.sep) : false;
56
+ if (!isHandler && !isMiddleware) continue;
57
+ await this.reloadWithRollback(fileToReload, rollbackEnabled);
58
+ }
59
+ }
60
+ async reloadWithRollback(file, rollbackEnabled) {
61
+ const { logger } = this.options;
62
+ const snapshot = this.snapshotUnits(file);
63
+ this.unload(file);
64
+ if (await this.reloadFile(file)) return;
65
+ this.unload(file);
66
+ if (!rollbackEnabled) return;
67
+ if (snapshot.handlers.length === 0 && snapshot.middlewares.length === 0) return;
68
+ this.restoreUnits(file, snapshot);
69
+ logger.warn(`${chalk.default.yellow.bold("Rolled back")} ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))} to the last-good version`);
70
+ }
71
+ /** Links the handler to its source file so a later update can unload it. */
72
+ trackHandler(file, handler) {
73
+ let set = this.store.fileToHandlers.get(file);
74
+ if (!set) {
75
+ set = /* @__PURE__ */ new Set();
76
+ this.store.fileToHandlers.set(file, set);
77
+ }
78
+ set.add(handler);
79
+ if (this.options.getArtifacts) {
80
+ const artifacts = this.options.getArtifacts(handler);
81
+ this.store.handlerArtifacts.set(handler, artifacts);
82
+ }
83
+ }
84
+ /** Links the middleware to its source file so a later update can unload it. */
85
+ trackMiddleware(file, middleware) {
86
+ let set = this.store.fileToMiddlewares.get(file);
87
+ if (!set) {
88
+ set = /* @__PURE__ */ new Set();
89
+ this.store.fileToMiddlewares.set(file, set);
90
+ }
91
+ set.add(middleware);
92
+ }
93
+ isTracked(file) {
94
+ return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);
95
+ }
96
+ unload(file) {
97
+ const { logger } = this.options;
98
+ const handlers = this.store.fileToHandlers.get(file);
99
+ if (handlers) {
100
+ for (const handler of handlers) {
101
+ const artifacts = this.store.handlerArtifacts.get(handler);
102
+ this.options.unregisterHandler(handler, artifacts);
103
+ this.store.handlerArtifacts.delete(handler);
104
+ const name = displayName(handler, "Handler");
105
+ logger.info(`${chalk.default.red.bold("Unloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
106
+ }
107
+ this.store.fileToHandlers.delete(file);
108
+ }
109
+ const middlewares = this.store.fileToMiddlewares.get(file);
110
+ if (middlewares) {
111
+ for (const middleware of middlewares) {
112
+ this.options.unregisterMiddleware?.(middleware);
113
+ const name = displayName(middleware, "Middleware");
114
+ logger.info(`${chalk.default.red.bold("Unloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
115
+ }
116
+ this.store.fileToMiddlewares.delete(file);
117
+ }
118
+ }
119
+ async reloadFile(file) {
120
+ const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;
121
+ if (!(0, node_fs.existsSync)(file)) {
122
+ logger.info(`File does not exist, skipping reload: ${(0, _seedcord_utils.formatFilePath)(file)}`);
123
+ return false;
124
+ }
125
+ try {
126
+ let fileUrl = (0, node_url.pathToFileURL)(file).href;
127
+ if (process.env.VITEST === "true") fileUrl += `?update=${Date.now()}`;
128
+ const imported = await import(fileUrl);
129
+ for (const val of Object.values(imported)) if (isHandler(val)) {
130
+ registerHandler(val, file);
131
+ this.trackHandler(file, val);
132
+ const name = displayName(val, "Handler");
133
+ logger.info(`${chalk.default.blue.bold("Reloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
134
+ } else if (isMiddleware && registerMiddleware && isMiddleware(val)) {
135
+ registerMiddleware(val, file);
136
+ this.trackMiddleware(file, val);
137
+ const name = displayName(val, "Middleware");
138
+ logger.info(`${chalk.default.blue.bold("Reloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
139
+ }
140
+ return true;
141
+ } catch (error) {
142
+ logger.error(`Failed to reload file: ${file}`, error);
143
+ return false;
144
+ }
145
+ }
146
+ snapshotUnits(file) {
147
+ return {
148
+ handlers: [...this.store.fileToHandlers.get(file) ?? []],
149
+ middlewares: [...this.store.fileToMiddlewares.get(file) ?? []]
150
+ };
151
+ }
152
+ restoreUnits(file, snapshot) {
153
+ for (const handler of snapshot.handlers) {
154
+ this.options.registerHandler(handler, file);
155
+ this.trackHandler(file, handler);
156
+ }
157
+ if (this.options.registerMiddleware) for (const middleware of snapshot.middlewares) {
158
+ this.options.registerMiddleware(middleware, file);
159
+ this.trackMiddleware(file, middleware);
160
+ }
161
+ }
162
+ };
163
+
164
+ //#endregion
165
+ exports.HmrModuleHandler = HmrModuleHandler;
166
+ //# sourceMappingURL=hmr.index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hmr.index.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":"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"}
@@ -0,0 +1 @@
1
+ export type * from './hmr.index.d.mts'
@@ -0,0 +1,44 @@
1
+ import { Logger } from "@seedcord/logger";
2
+ import { HmrUpdateEvent } from "@seedcord/types";
3
+
4
+ //#region src/hmr/HmrModuleHandler.d.ts
5
+ interface HmrModuleHandlerOptions<THandler, TMiddleware = void, TArtifacts = unknown> {
6
+ handlersDir: string;
7
+ middlewaresDir?: string;
8
+ isHandler: (val: unknown) => val is THandler;
9
+ isMiddleware?: (val: unknown) => val is TMiddleware;
10
+ registerHandler: (handler: THandler, file: string) => void;
11
+ registerMiddleware?: (middleware: TMiddleware, file: string) => void;
12
+ unregisterHandler: (handler: THandler, artifacts?: TArtifacts) => void;
13
+ unregisterMiddleware?: (middleware: TMiddleware) => void;
14
+ /** Snapshotted per handler so a rollback re-registers with the same artifacts after the module re-imports. */
15
+ getArtifacts?: (handler: THandler) => TArtifacts;
16
+ logger: Logger;
17
+ }
18
+ /**
19
+ * Reloads THandler and optional TMiddleware modules on HMR updates.
20
+ *
21
+ * Call `handle()` from your `HmrAware.onHmr()` for standard reload behavior, or write a fully custom `onHmr()` and skip this class.
22
+ *
23
+ * This is only useful during development.
24
+ */
25
+ declare class HmrModuleHandler<THandler, TMiddleware = void, TArtifacts = unknown> {
26
+ private readonly store;
27
+ private readonly options;
28
+ constructor(options: HmrModuleHandlerOptions<THandler, TMiddleware, TArtifacts>);
29
+ /** Unloads or reloads the modules affected by an {@link HmrUpdateEvent}, scoped to the handler and middleware directories. */
30
+ handle(event: HmrUpdateEvent): Promise<void>;
31
+ private reloadWithRollback;
32
+ /** Links the handler to its source file so a later update can unload it. */
33
+ trackHandler(file: string, handler: THandler): void;
34
+ /** Links the middleware to its source file so a later update can unload it. */
35
+ trackMiddleware(file: string, middleware: TMiddleware): void;
36
+ private isTracked;
37
+ private unload;
38
+ private reloadFile;
39
+ private snapshotUnits;
40
+ private restoreUnits;
41
+ }
42
+ //#endregion
43
+ export { HmrModuleHandler };
44
+ //# sourceMappingURL=hmr.index.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 };
163
+ //# sourceMappingURL=hmr.index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hmr.index.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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"}
package/dist/index.cjs CHANGED
@@ -432,7 +432,7 @@ function paginate(items, page, perPage) {
432
432
  //#endregion
433
433
  //#region src/index.ts
434
434
  /** Package version */
435
- const version = "0.1.0-next.3";
435
+ const version = "0.1.0-next.4";
436
436
 
437
437
  //#endregion
438
438
  exports.BuilderComponent = require_CustomId.BuilderComponent;
package/dist/index.mjs CHANGED
@@ -431,7 +431,7 @@ function paginate(items, page, perPage) {
431
431
  //#endregion
432
432
  //#region src/index.ts
433
433
  /** Package version */
434
- const version = "0.1.0-next.3";
434
+ const version = "0.1.0-next.4";
435
435
 
436
436
  //#endregion
437
437
  export { BuilderComponent, Cooldown, CustomId, DispatchContext, DmOnly, Fault, GuildOnly, Notice, OwnerOnly, RegisterCommand, RowComponent, Silence, and, defineEffectGate, defineGate, or, paginate, version };
@@ -1,5 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_rolldown_runtime = require('./rolldown-runtime-C_NdSu1c.cjs');
2
3
  const require_CustomId = require('./CustomId-DV7daU7r.cjs');
4
+ let _seedcord_utils = require("@seedcord/utils");
5
+ let _seedcord_logger = require("@seedcord/logger");
6
+ let chalk = require("chalk");
7
+ chalk = require_rolldown_runtime.__toESM(chalk, 1);
8
+ let envapt = require("envapt");
3
9
 
4
10
  //#region src/gates/runGates.ts
5
11
  function routeIdOf(handlerCtor) {
@@ -30,6 +36,72 @@ async function runHandlerGates(handlerCtor, ctx) {
30
36
  });
31
37
  }
32
38
 
39
+ //#endregion
40
+ //#region src/hmr/devChannel.ts
41
+ let channel;
42
+ /** @internal */
43
+ function setDevChannel(next) {
44
+ channel = next;
45
+ }
46
+ /** @internal */
47
+ function getDevChannel() {
48
+ return channel;
49
+ }
50
+
51
+ //#endregion
52
+ //#region src/hmr/wrapHot.ts
53
+ /**
54
+ * Wraps a raw vite hot object as a typed {@link DevChannel}.
55
+ *
56
+ * @internal
57
+ */
58
+ function wrapHot(hot) {
59
+ return {
60
+ send: (event, data) => hot.send(event, data),
61
+ on: (event, cb) => hot.on(event, cb)
62
+ };
63
+ }
64
+
65
+ //#endregion
66
+ //#region src/hmr/HmrManager.ts
67
+ var HmrManager = class {
68
+ logger = new _seedcord_logger.Logger("HMR", { channel: "hmr" });
69
+ listeners = /* @__PURE__ */ new Set();
70
+ /** @internal */
71
+ init() {
72
+ if (!{}.hot) return;
73
+ const channel = wrapHot({}.hot);
74
+ setDevChannel(channel);
75
+ if (envapt.Envapter.isDevelopment || envapt.Envapter.isTest) {
76
+ this.logger.info("Enabled");
77
+ channel.on("seedcord:hmr", (payload) => {
78
+ const affected = payload.affectedModules?.length ?? 0;
79
+ this.logger.info(`${chalk.default.bold("1")} module changed, ${chalk.default.bold(affected)} affected modules`);
80
+ if (payload.affectedModules) this.logger.utils.list(payload.affectedModules.map((mod) => (0, _seedcord_utils.formatFilePath)(mod)), "Affected modules: ");
81
+ this.handleUpdate(payload);
82
+ });
83
+ }
84
+ }
85
+ /** @internal */
86
+ register(listener) {
87
+ this.listeners.add(listener);
88
+ }
89
+ /** @internal */
90
+ unregister(listener) {
91
+ this.listeners.delete(listener);
92
+ }
93
+ async handleUpdate(event) {
94
+ const promises = [...this.listeners].map(async (listener) => {
95
+ try {
96
+ await listener.onHmr(event);
97
+ } catch (error) {
98
+ this.logger.error(`Error handling HMR update in ${chalk.default.bold(listener.name)}: `, error);
99
+ }
100
+ });
101
+ await Promise.all(promises);
102
+ }
103
+ };
104
+
33
105
  //#endregion
34
106
  //#region src/customId/routing.ts
35
107
  /**
@@ -55,6 +127,7 @@ exports.ComponentDefsKey = ComponentDefsKey;
55
127
  exports.EventMetadataKey = require_CustomId.EventMetadataKey;
56
128
  exports.GateNotice = require_CustomId.GateNotice;
57
129
  exports.GatedMetadataKey = require_CustomId.GatedMetadataKey;
130
+ exports.HmrManager = HmrManager;
58
131
  exports.InteractionMetadataKey = require_CustomId.InteractionMetadataKey;
59
132
  exports.InteractionRouteKeys = require_CustomId.InteractionRouteKeys;
60
133
  exports.InteractionRoutes = require_CustomId.InteractionRoutes;
@@ -69,6 +142,7 @@ exports.OnCooldown = require_CustomId.OnCooldown;
69
142
  exports.PAGE_MAX = PAGE_MAX;
70
143
  exports.SubscribeMetadataKey = require_CustomId.SubscribeMetadataKey;
71
144
  exports.decodeFor = require_CustomId.decodeFor;
145
+ exports.getDevChannel = getDevChannel;
72
146
  exports.pageCursor = pageCursor;
73
147
  exports.pickNotice = require_CustomId.pickNotice;
74
148
  exports.prefixOf = require_CustomId.prefixOf;
@@ -76,4 +150,6 @@ exports.routeIdOf = routeIdOf;
76
150
  exports.runGates = runGates;
77
151
  exports.runHandlerGates = runHandlerGates;
78
152
  exports.setBotColor = require_CustomId.setBotColor;
153
+ exports.setDevChannel = setDevChannel;
154
+ exports.wrapHot = wrapHot;
79
155
  //# sourceMappingURL=internal.index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal.index.cjs","names":["InteractionRouteKeys","runCheck","runCommits","GatedMetadataKey","CustomId"],"sources":["../src/gates/runGates.ts","../src/customId/routing.ts","../src/pagination/cursor.ts"],"sourcesContent":["import { GatedMetadataKey, InteractionRouteKeys } from '@src/metadataKeys';\n\nimport { discardCommits, runCheck, runCommits } from './effects';\n\nimport type { Gate, GateContextBase } from './Gate';\n\n// the stable id a route decorator stored, e.g. slash:daily or button:confirm. null for a plain event handler.\nexport function routeIdOf(handlerCtor: object): string | null {\n for (const [kind, key] of Object.entries(InteractionRouteKeys)) {\n // justified: getMetadata returns any, this key only ever stores the route/prefix string array\n const routes = Reflect.getMetadata(key, handlerCtor) as string[] | undefined;\n if (routes?.length) return `${kind}:${routes.join(',')}`;\n }\n return null;\n}\n\n/**\n * Runs each gate's check in order, so the first refusal propagates to the dispatcher boundary. An effect\n * gate's commit runs once the whole set passes.\n */\nexport async function runGates(gates: readonly Gate<GateContextBase>[], ctx: GateContextBase): Promise<void> {\n try {\n for (const gate of gates) {\n await runCheck(gate, ctx);\n }\n await runCommits(ctx);\n } finally {\n discardCommits(ctx);\n }\n}\n\n// dispatchers call this before execute, inside the boundary, so a refusal renders or drops\nexport async function runHandlerGates(handlerCtor: object, ctx: GateContextBase): Promise<void> {\n // justified: getMetadata returns any, and this key only ever stores the @Gated gate array\n const gates = Reflect.getMetadata(GatedMetadataKey, handlerCtor) as readonly Gate<GateContextBase>[] | undefined;\n if (!gates) return;\n await runGates(gates, { ...ctx, routeId: routeIdOf(handlerCtor) });\n}\n","import type { AnyCustomId } from './CustomId';\n\n/**\n * The route decorators store a handler's customId definitions here so the component base can decode\n * against them at runtime.\n *\n * @internal\n */\nexport const ComponentDefsKey = Symbol('seedcord:customId:componentDefs');\n\n/**\n * The phantom a component handler base carries. A route decorator constrains its argument to this, so\n * passing different definitions to the decorator and the handler's generic is a compile error. Never set at runtime.\n *\n * @internal\n */\nexport interface HasComponentDefs<Defs extends readonly AnyCustomId[]> {\n readonly __componentDefs?: Defs;\n}\n","import { CustomId } from '@customId/CustomId';\n\nimport type { CustomIdField } from '@customId/Field';\n\n// Included in the layout hash so it is fixed per prefix. Reducing it marks every active button as StaleCustomId.\n// @internal\nexport const PAGE_MAX = 1_000_000;\n\n// Discord rejects a message with duplicate custom_ids, and two controls can target the same page (first and\n// prev both hit 0), so each control uses a distinct slot.\n// @internal\nconst SLOT_MAX = 4;\n\n/** Page cursor for a paginator. */\nexport type PageCursor<Prefix extends string> = CustomId<\n Prefix,\n { page: CustomIdField<number>; slot: CustomIdField<number> }\n>;\n\n/** Build the page cursor for a paginator. */\nexport function pageCursor<Prefix extends string>(prefix: Prefix): PageCursor<Prefix> {\n return new CustomId(prefix).int('page', 0, PAGE_MAX).int('slot', 0, SLOT_MAX);\n}\n"],"mappings":";;;;AAOA,SAAgB,UAAU,aAAoC;CAC1D,KAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQA,qCAAoB,GAAG;EAE5D,MAAM,SAAS,QAAQ,YAAY,KAAK,WAAW;EACnD,IAAI,QAAQ,QAAQ,OAAO,GAAG,KAAK,GAAG,OAAO,KAAK,GAAG;CACzD;CACA,OAAO;AACX;;;;;AAMA,eAAsB,SAAS,OAAyC,KAAqC;CACzG,IAAI;EACA,KAAK,MAAM,QAAQ,OACf,MAAMC,0BAAS,MAAM,GAAG;EAE5B,MAAMC,4BAAW,GAAG;CACxB,UAAU;EACN,gCAAe,GAAG;CACtB;AACJ;AAGA,eAAsB,gBAAgB,aAAqB,KAAqC;CAE5F,MAAM,QAAQ,QAAQ,YAAYC,mCAAkB,WAAW;CAC/D,IAAI,CAAC,OAAO;CACZ,MAAM,SAAS,OAAO;EAAE,GAAG;EAAK,SAAS,UAAU,WAAW;CAAE,CAAC;AACrE;;;;;;;;;;AC7BA,MAAa,mBAAmB,OAAO,iCAAiC;;;;ACFxE,MAAa,WAAW;AAKxB,MAAM,WAAW;;AASjB,SAAgB,WAAkC,QAAoC;CAClF,OAAO,IAAIC,0BAAS,MAAM,CAAC,CAAC,IAAI,QAAQ,GAAG,QAAQ,CAAC,CAAC,IAAI,QAAQ,GAAG,QAAQ;AAChF"}
1
+ {"version":3,"file":"internal.index.cjs","names":["InteractionRouteKeys","runCheck","runCommits","GatedMetadataKey","Logger","Envapter","CustomId"],"sources":["../src/gates/runGates.ts","../src/hmr/devChannel.ts","../src/hmr/wrapHot.ts","../src/hmr/HmrManager.ts","../src/customId/routing.ts","../src/pagination/cursor.ts"],"sourcesContent":["import { GatedMetadataKey, InteractionRouteKeys } from '@src/metadataKeys';\n\nimport { discardCommits, runCheck, runCommits } from './effects';\n\nimport type { Gate, GateContextBase } from './Gate';\n\n// the stable id a route decorator stored, e.g. slash:daily or button:confirm. null for a plain event handler.\nexport function routeIdOf(handlerCtor: object): string | null {\n for (const [kind, key] of Object.entries(InteractionRouteKeys)) {\n // justified: getMetadata returns any, this key only ever stores the route/prefix string array\n const routes = Reflect.getMetadata(key, handlerCtor) as string[] | undefined;\n if (routes?.length) return `${kind}:${routes.join(',')}`;\n }\n return null;\n}\n\n/**\n * Runs each gate's check in order, so the first refusal propagates to the dispatcher boundary. An effect\n * gate's commit runs once the whole set passes.\n */\nexport async function runGates(gates: readonly Gate<GateContextBase>[], ctx: GateContextBase): Promise<void> {\n try {\n for (const gate of gates) {\n await runCheck(gate, ctx);\n }\n await runCommits(ctx);\n } finally {\n discardCommits(ctx);\n }\n}\n\n// dispatchers call this before execute, inside the boundary, so a refusal renders or drops\nexport async function runHandlerGates(handlerCtor: object, ctx: GateContextBase): Promise<void> {\n // justified: getMetadata returns any, and this key only ever stores the @Gated gate array\n const gates = Reflect.getMetadata(GatedMetadataKey, handlerCtor) as readonly Gate<GateContextBase>[] | undefined;\n if (!gates) return;\n await runGates(gates, { ...ctx, routeId: routeIdOf(handlerCtor) });\n}\n","import type { DevChannel, SeedcordCliEvents, SeedcordFrameworkEvents } from '@seedcord/types/internal';\n\n// one module instance per process, the HmrManager write and every framework read share it\nlet channel: DevChannel<SeedcordFrameworkEvents, SeedcordCliEvents> | undefined;\n\n/** @internal */\nexport function setDevChannel(next: DevChannel<SeedcordFrameworkEvents, SeedcordCliEvents> | undefined): void {\n channel = next;\n}\n\n/** @internal */\nexport function getDevChannel(): DevChannel<SeedcordFrameworkEvents, SeedcordCliEvents> | undefined {\n return channel;\n}\n","import type { DevChannel } from '@seedcord/types/internal';\n\n// the minimal raw-hot shape, satisfied by both vite's `import.meta.hot` and the CLI's `NormalizedHotChannel`.\ninterface RawHot {\n send(event: string, data?: unknown): void;\n on(event: string, cb: (data: unknown) => void): void;\n}\n\n/**\n * Wraps a raw vite hot object as a typed {@link DevChannel}.\n *\n * @internal\n */\nexport function wrapHot<TSend, TRecv>(hot: RawHot): DevChannel<TSend, TRecv> {\n return {\n send: (event, data) => hot.send(event, data),\n // justified: RawHot types every payload as unknown, the wire contract guarantees it matches TRecv[Key]\n on: (event, cb) => hot.on(event, cb as (data: unknown) => void)\n };\n}\n","// scoped here so import.meta.hot stays untyped in the rest of the package\n/// <reference types=\"vite/client\" />\nimport { Logger } from '@seedcord/logger';\nimport { formatFilePath } from '@seedcord/utils';\nimport chalk from 'chalk';\nimport { Envapter } from 'envapt';\n\nimport { setDevChannel } from './devChannel';\nimport { wrapHot } from './wrapHot';\n\nimport type { HmrAware, HmrUpdateEvent } from '@seedcord/types';\nimport type { SeedcordCliEvents, SeedcordFrameworkEvents } from '@seedcord/types/internal';\n\nexport class HmrManager {\n private readonly logger = new Logger('HMR', { channel: 'hmr' });\n private readonly listeners = new Set<HmrAware>();\n\n /** @internal */\n public init(): void {\n if (!import.meta.hot) return;\n // the only raw `import.meta.hot` read in the framework\n const channel = wrapHot<SeedcordFrameworkEvents, SeedcordCliEvents>(import.meta.hot);\n setDevChannel(channel);\n\n if (Envapter.isDevelopment || Envapter.isTest) {\n this.logger.info('Enabled');\n\n channel.on('seedcord:hmr', (payload) => {\n const affected = payload.affectedModules?.length ?? 0;\n this.logger.info(`${chalk.bold('1')} module changed, ${chalk.bold(affected)} affected modules`);\n\n if (payload.affectedModules) {\n this.logger.utils.list(\n payload.affectedModules.map((mod) => formatFilePath(mod)),\n 'Affected modules: '\n );\n }\n\n void this.handleUpdate(payload);\n });\n }\n }\n\n /** @internal */\n public register(listener: HmrAware): void {\n this.listeners.add(listener);\n }\n\n /** @internal */\n public unregister(listener: HmrAware): void {\n this.listeners.delete(listener);\n }\n\n private async handleUpdate(event: HmrUpdateEvent): Promise<void> {\n const promises = [...this.listeners].map(async (listener) => {\n try {\n await listener.onHmr(event);\n } catch (error) {\n this.logger.error(`Error handling HMR update in ${chalk.bold(listener.name)}: `, error);\n }\n });\n\n await Promise.all(promises);\n }\n}\n","import type { AnyCustomId } from './CustomId';\n\n/**\n * The route decorators store a handler's customId definitions here so the component base can decode\n * against them at runtime.\n *\n * @internal\n */\nexport const ComponentDefsKey = Symbol('seedcord:customId:componentDefs');\n\n/**\n * The phantom a component handler base carries. A route decorator constrains its argument to this, so\n * passing different definitions to the decorator and the handler's generic is a compile error. Never set at runtime.\n *\n * @internal\n */\nexport interface HasComponentDefs<Defs extends readonly AnyCustomId[]> {\n readonly __componentDefs?: Defs;\n}\n","import { CustomId } from '@customId/CustomId';\n\nimport type { CustomIdField } from '@customId/Field';\n\n// Included in the layout hash so it is fixed per prefix. Reducing it marks every active button as StaleCustomId.\n// @internal\nexport const PAGE_MAX = 1_000_000;\n\n// Discord rejects a message with duplicate custom_ids, and two controls can target the same page (first and\n// prev both hit 0), so each control uses a distinct slot.\n// @internal\nconst SLOT_MAX = 4;\n\n/** Page cursor for a paginator. */\nexport type PageCursor<Prefix extends string> = CustomId<\n Prefix,\n { page: CustomIdField<number>; slot: CustomIdField<number> }\n>;\n\n/** Build the page cursor for a paginator. */\nexport function pageCursor<Prefix extends string>(prefix: Prefix): PageCursor<Prefix> {\n return new CustomId(prefix).int('page', 0, PAGE_MAX).int('slot', 0, SLOT_MAX);\n}\n"],"mappings":";;;;;;;;;;AAOA,SAAgB,UAAU,aAAoC;CAC1D,KAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQA,qCAAoB,GAAG;EAE5D,MAAM,SAAS,QAAQ,YAAY,KAAK,WAAW;EACnD,IAAI,QAAQ,QAAQ,OAAO,GAAG,KAAK,GAAG,OAAO,KAAK,GAAG;CACzD;CACA,OAAO;AACX;;;;;AAMA,eAAsB,SAAS,OAAyC,KAAqC;CACzG,IAAI;EACA,KAAK,MAAM,QAAQ,OACf,MAAMC,0BAAS,MAAM,GAAG;EAE5B,MAAMC,4BAAW,GAAG;CACxB,UAAU;EACN,gCAAe,GAAG;CACtB;AACJ;AAGA,eAAsB,gBAAgB,aAAqB,KAAqC;CAE5F,MAAM,QAAQ,QAAQ,YAAYC,mCAAkB,WAAW;CAC/D,IAAI,CAAC,OAAO;CACZ,MAAM,SAAS,OAAO;EAAE,GAAG;EAAK,SAAS,UAAU,WAAW;CAAE,CAAC;AACrE;;;;AClCA,IAAI;;AAGJ,SAAgB,cAAc,MAAgF;CAC1G,UAAU;AACd;;AAGA,SAAgB,gBAAoF;CAChG,OAAO;AACX;;;;;;;;;ACAA,SAAgB,QAAsB,KAAuC;CACzE,OAAO;EACH,OAAO,OAAO,SAAS,IAAI,KAAK,OAAO,IAAI;EAE3C,KAAK,OAAO,OAAO,IAAI,GAAG,OAAO,EAA6B;CAClE;AACJ;;;;ACNA,IAAa,aAAb,MAAwB;CACpB,AAAiB,SAAS,IAAIC,wBAAO,OAAO,EAAE,SAAS,MAAM,CAAC;CAC9D,AAAiB,4BAAY,IAAI,IAAc;;CAG/C,AAAO,OAAa;EAChB,IAAI,IAAa,KAAK;EAEtB,MAAM,UAAU,WAAgE,GAAG;EACnF,cAAc,OAAO;EAErB,IAAIC,gBAAS,iBAAiBA,gBAAS,QAAQ;GAC3C,KAAK,OAAO,KAAK,SAAS;GAE1B,QAAQ,GAAG,iBAAiB,YAAY;IACpC,MAAM,WAAW,QAAQ,iBAAiB,UAAU;IACpD,KAAK,OAAO,KAAK,GAAG,cAAM,KAAK,GAAG,EAAE,mBAAmB,cAAM,KAAK,QAAQ,EAAE,kBAAkB;IAE9F,IAAI,QAAQ,iBACR,KAAK,OAAO,MAAM,KACd,QAAQ,gBAAgB,KAAK,4CAAuB,GAAG,CAAC,GACxD,oBACJ;IAGJ,AAAK,KAAK,aAAa,OAAO;GAClC,CAAC;EACL;CACJ;;CAGA,AAAO,SAAS,UAA0B;EACtC,KAAK,UAAU,IAAI,QAAQ;CAC/B;;CAGA,AAAO,WAAW,UAA0B;EACxC,KAAK,UAAU,OAAO,QAAQ;CAClC;CAEA,MAAc,aAAa,OAAsC;EAC7D,MAAM,WAAW,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,IAAI,OAAO,aAAa;GACzD,IAAI;IACA,MAAM,SAAS,MAAM,KAAK;GAC9B,SAAS,OAAO;IACZ,KAAK,OAAO,MAAM,gCAAgC,cAAM,KAAK,SAAS,IAAI,EAAE,KAAK,KAAK;GAC1F;EACJ,CAAC;EAED,MAAM,QAAQ,IAAI,QAAQ;CAC9B;AACJ;;;;;;;;;;ACxDA,MAAa,mBAAmB,OAAO,iCAAiC;;;;ACFxE,MAAa,WAAW;AAKxB,MAAM,WAAW;;AASjB,SAAgB,WAAkC,QAAoC;CAClF,OAAO,IAAIC,0BAAS,MAAM,CAAC,CAAC,IAAI,QAAQ,GAAG,QAAQ,CAAC,CAAC,IAAI,QAAQ,GAAG,QAAQ;AAChF"}
@@ -1,5 +1,6 @@
1
1
  import { C as BuilderComponent, a as CustomIdField, c as pickNotice, g as GateContextBase, h as Gate, i as prefixOf, l as GateFitsWith, n as CustomId, o as DecodedParams, p as Notice, r as decodeFor, t as AnyCustomId, x as CommandMeta } from "./CustomId--QizzBOn.mjs";
2
- import { BotColor, EpochMs, ReplyResponse } from "@seedcord/types";
2
+ import { BotColor, EpochMs, HmrAware, ReplyResponse } from "@seedcord/types";
3
+ import { DevChannel, SeedcordCliEvents, SeedcordFrameworkEvents } from "@seedcord/types/internal";
3
4
 
4
5
  //#region src/components/botColorHolder.d.ts
5
6
  declare function setBotColor(color: BotColor | undefined): void;
@@ -13,6 +14,37 @@ declare function routeIdOf(handlerCtor: object): string | null;
13
14
  declare function runGates(gates: readonly Gate<GateContextBase>[], ctx: GateContextBase): Promise<void>;
14
15
  declare function runHandlerGates(handlerCtor: object, ctx: GateContextBase): Promise<void>;
15
16
  //#endregion
17
+ //#region src/hmr/devChannel.d.ts
18
+ /** @internal */
19
+ declare function setDevChannel(next: DevChannel<SeedcordFrameworkEvents, SeedcordCliEvents> | undefined): void;
20
+ /** @internal */
21
+ declare function getDevChannel(): DevChannel<SeedcordFrameworkEvents, SeedcordCliEvents> | undefined;
22
+ //#endregion
23
+ //#region src/hmr/HmrManager.d.ts
24
+ declare class HmrManager {
25
+ private readonly logger;
26
+ private readonly listeners;
27
+ /** @internal */
28
+ init(): void;
29
+ /** @internal */
30
+ register(listener: HmrAware): void;
31
+ /** @internal */
32
+ unregister(listener: HmrAware): void;
33
+ private handleUpdate;
34
+ }
35
+ //#endregion
36
+ //#region src/hmr/wrapHot.d.ts
37
+ interface RawHot {
38
+ send(event: string, data?: unknown): void;
39
+ on(event: string, cb: (data: unknown) => void): void;
40
+ }
41
+ /**
42
+ * Wraps a raw vite hot object as a typed {@link DevChannel}.
43
+ *
44
+ * @internal
45
+ */
46
+ declare function wrapHot<TSend, TRecv>(hot: RawHot): DevChannel<TSend, TRecv>;
47
+ //#endregion
16
48
  //#region src/notices/index.d.ts
17
49
  declare abstract class GateNotice extends Notice {
18
50
  render(): ReplyResponse;
@@ -99,5 +131,5 @@ type PageCursor<Prefix extends string> = CustomId<Prefix, {
99
131
  /** Build the page cursor for a paginator. */
100
132
  declare function pageCursor<Prefix extends string>(prefix: Prefix): PageCursor<Prefix>;
101
133
  //#endregion
102
- export { type AnyCustomId, type CommandMeta, CommandMetadataKey, ComponentDefsKey, type DecodedParams, EventMetadataKey, type GateFitsWith, GateNotice, GatedMetadataKey, type HasComponentDefs, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, NeedsAny, NotAllowed, NotInDm, NotInGuild, NotOwner, NoticeCard, OnCooldown, PAGE_MAX, type PageCursor, SubscribeMetadataKey, decodeFor, pageCursor, pickNotice, prefixOf, routeIdOf, runGates, runHandlerGates, setBotColor };
134
+ export { type AnyCustomId, type CommandMeta, CommandMetadataKey, ComponentDefsKey, type DecodedParams, EventMetadataKey, type GateFitsWith, GateNotice, GatedMetadataKey, type HasComponentDefs, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, NeedsAny, NotAllowed, NotInDm, NotInGuild, NotOwner, NoticeCard, OnCooldown, PAGE_MAX, type PageCursor, SubscribeMetadataKey, decodeFor, getDevChannel, pageCursor, pickNotice, prefixOf, routeIdOf, runGates, runHandlerGates, setBotColor, setDevChannel, wrapHot };
103
135
  //# sourceMappingURL=internal.index.d.mts.map
@@ -1,4 +1,8 @@
1
1
  import { C as EventMetadataKey, D as InteractionRoutes, E as InteractionRouteKeys, O as MiddlewareMetadataKey, S as CommandMetadataKey, T as InteractionMetadataKey, _ as NoticeCard, a as discardCommits, b as setBotColor, c as runCheck, d as NeedsAny, f as NotAllowed, g as OnCooldown, h as NotOwner, i as pickNotice, k as SubscribeMetadataKey, l as runCommits, m as NotInGuild, n as decodeFor, p as NotInDm, r as prefixOf, t as CustomId, u as GateNotice, w as GatedMetadataKey } from "./CustomId-epJayEGO.mjs";
2
+ import { formatFilePath } from "@seedcord/utils";
3
+ import { Logger } from "@seedcord/logger";
4
+ import chalk from "chalk";
5
+ import { Envapter } from "envapt";
2
6
 
3
7
  //#region src/gates/runGates.ts
4
8
  function routeIdOf(handlerCtor) {
@@ -29,6 +33,72 @@ async function runHandlerGates(handlerCtor, ctx) {
29
33
  });
30
34
  }
31
35
 
36
+ //#endregion
37
+ //#region src/hmr/devChannel.ts
38
+ let channel;
39
+ /** @internal */
40
+ function setDevChannel(next) {
41
+ channel = next;
42
+ }
43
+ /** @internal */
44
+ function getDevChannel() {
45
+ return channel;
46
+ }
47
+
48
+ //#endregion
49
+ //#region src/hmr/wrapHot.ts
50
+ /**
51
+ * Wraps a raw vite hot object as a typed {@link DevChannel}.
52
+ *
53
+ * @internal
54
+ */
55
+ function wrapHot(hot) {
56
+ return {
57
+ send: (event, data) => hot.send(event, data),
58
+ on: (event, cb) => hot.on(event, cb)
59
+ };
60
+ }
61
+
62
+ //#endregion
63
+ //#region src/hmr/HmrManager.ts
64
+ var HmrManager = class {
65
+ logger = new Logger("HMR", { channel: "hmr" });
66
+ listeners = /* @__PURE__ */ new Set();
67
+ /** @internal */
68
+ init() {
69
+ if (!import.meta.hot) return;
70
+ const channel = wrapHot(import.meta.hot);
71
+ setDevChannel(channel);
72
+ if (Envapter.isDevelopment || Envapter.isTest) {
73
+ this.logger.info("Enabled");
74
+ channel.on("seedcord:hmr", (payload) => {
75
+ const affected = payload.affectedModules?.length ?? 0;
76
+ this.logger.info(`${chalk.bold("1")} module changed, ${chalk.bold(affected)} affected modules`);
77
+ if (payload.affectedModules) this.logger.utils.list(payload.affectedModules.map((mod) => formatFilePath(mod)), "Affected modules: ");
78
+ this.handleUpdate(payload);
79
+ });
80
+ }
81
+ }
82
+ /** @internal */
83
+ register(listener) {
84
+ this.listeners.add(listener);
85
+ }
86
+ /** @internal */
87
+ unregister(listener) {
88
+ this.listeners.delete(listener);
89
+ }
90
+ async handleUpdate(event) {
91
+ const promises = [...this.listeners].map(async (listener) => {
92
+ try {
93
+ await listener.onHmr(event);
94
+ } catch (error) {
95
+ this.logger.error(`Error handling HMR update in ${chalk.bold(listener.name)}: `, error);
96
+ }
97
+ });
98
+ await Promise.all(promises);
99
+ }
100
+ };
101
+
32
102
  //#endregion
33
103
  //#region src/customId/routing.ts
34
104
  /**
@@ -49,5 +119,5 @@ function pageCursor(prefix) {
49
119
  }
50
120
 
51
121
  //#endregion
52
- export { CommandMetadataKey, ComponentDefsKey, EventMetadataKey, GateNotice, GatedMetadataKey, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, NeedsAny, NotAllowed, NotInDm, NotInGuild, NotOwner, NoticeCard, OnCooldown, PAGE_MAX, SubscribeMetadataKey, decodeFor, pageCursor, pickNotice, prefixOf, routeIdOf, runGates, runHandlerGates, setBotColor };
122
+ export { CommandMetadataKey, ComponentDefsKey, EventMetadataKey, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, NeedsAny, NotAllowed, NotInDm, NotInGuild, NotOwner, NoticeCard, OnCooldown, PAGE_MAX, SubscribeMetadataKey, decodeFor, getDevChannel, pageCursor, pickNotice, prefixOf, routeIdOf, runGates, runHandlerGates, setBotColor, setDevChannel, wrapHot };
53
123
  //# sourceMappingURL=internal.index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal.index.mjs","names":[],"sources":["../src/gates/runGates.ts","../src/customId/routing.ts","../src/pagination/cursor.ts"],"sourcesContent":["import { GatedMetadataKey, InteractionRouteKeys } from '@src/metadataKeys';\n\nimport { discardCommits, runCheck, runCommits } from './effects';\n\nimport type { Gate, GateContextBase } from './Gate';\n\n// the stable id a route decorator stored, e.g. slash:daily or button:confirm. null for a plain event handler.\nexport function routeIdOf(handlerCtor: object): string | null {\n for (const [kind, key] of Object.entries(InteractionRouteKeys)) {\n // justified: getMetadata returns any, this key only ever stores the route/prefix string array\n const routes = Reflect.getMetadata(key, handlerCtor) as string[] | undefined;\n if (routes?.length) return `${kind}:${routes.join(',')}`;\n }\n return null;\n}\n\n/**\n * Runs each gate's check in order, so the first refusal propagates to the dispatcher boundary. An effect\n * gate's commit runs once the whole set passes.\n */\nexport async function runGates(gates: readonly Gate<GateContextBase>[], ctx: GateContextBase): Promise<void> {\n try {\n for (const gate of gates) {\n await runCheck(gate, ctx);\n }\n await runCommits(ctx);\n } finally {\n discardCommits(ctx);\n }\n}\n\n// dispatchers call this before execute, inside the boundary, so a refusal renders or drops\nexport async function runHandlerGates(handlerCtor: object, ctx: GateContextBase): Promise<void> {\n // justified: getMetadata returns any, and this key only ever stores the @Gated gate array\n const gates = Reflect.getMetadata(GatedMetadataKey, handlerCtor) as readonly Gate<GateContextBase>[] | undefined;\n if (!gates) return;\n await runGates(gates, { ...ctx, routeId: routeIdOf(handlerCtor) });\n}\n","import type { AnyCustomId } from './CustomId';\n\n/**\n * The route decorators store a handler's customId definitions here so the component base can decode\n * against them at runtime.\n *\n * @internal\n */\nexport const ComponentDefsKey = Symbol('seedcord:customId:componentDefs');\n\n/**\n * The phantom a component handler base carries. A route decorator constrains its argument to this, so\n * passing different definitions to the decorator and the handler's generic is a compile error. Never set at runtime.\n *\n * @internal\n */\nexport interface HasComponentDefs<Defs extends readonly AnyCustomId[]> {\n readonly __componentDefs?: Defs;\n}\n","import { CustomId } from '@customId/CustomId';\n\nimport type { CustomIdField } from '@customId/Field';\n\n// Included in the layout hash so it is fixed per prefix. Reducing it marks every active button as StaleCustomId.\n// @internal\nexport const PAGE_MAX = 1_000_000;\n\n// Discord rejects a message with duplicate custom_ids, and two controls can target the same page (first and\n// prev both hit 0), so each control uses a distinct slot.\n// @internal\nconst SLOT_MAX = 4;\n\n/** Page cursor for a paginator. */\nexport type PageCursor<Prefix extends string> = CustomId<\n Prefix,\n { page: CustomIdField<number>; slot: CustomIdField<number> }\n>;\n\n/** Build the page cursor for a paginator. */\nexport function pageCursor<Prefix extends string>(prefix: Prefix): PageCursor<Prefix> {\n return new CustomId(prefix).int('page', 0, PAGE_MAX).int('slot', 0, SLOT_MAX);\n}\n"],"mappings":";;;AAOA,SAAgB,UAAU,aAAoC;CAC1D,KAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQ,oBAAoB,GAAG;EAE5D,MAAM,SAAS,QAAQ,YAAY,KAAK,WAAW;EACnD,IAAI,QAAQ,QAAQ,OAAO,GAAG,KAAK,GAAG,OAAO,KAAK,GAAG;CACzD;CACA,OAAO;AACX;;;;;AAMA,eAAsB,SAAS,OAAyC,KAAqC;CACzG,IAAI;EACA,KAAK,MAAM,QAAQ,OACf,MAAM,SAAS,MAAM,GAAG;EAE5B,MAAM,WAAW,GAAG;CACxB,UAAU;EACN,eAAe,GAAG;CACtB;AACJ;AAGA,eAAsB,gBAAgB,aAAqB,KAAqC;CAE5F,MAAM,QAAQ,QAAQ,YAAY,kBAAkB,WAAW;CAC/D,IAAI,CAAC,OAAO;CACZ,MAAM,SAAS,OAAO;EAAE,GAAG;EAAK,SAAS,UAAU,WAAW;CAAE,CAAC;AACrE;;;;;;;;;;AC7BA,MAAa,mBAAmB,OAAO,iCAAiC;;;;ACFxE,MAAa,WAAW;AAKxB,MAAM,WAAW;;AASjB,SAAgB,WAAkC,QAAoC;CAClF,OAAO,IAAI,SAAS,MAAM,CAAC,CAAC,IAAI,QAAQ,GAAG,QAAQ,CAAC,CAAC,IAAI,QAAQ,GAAG,QAAQ;AAChF"}
1
+ {"version":3,"file":"internal.index.mjs","names":[],"sources":["../src/gates/runGates.ts","../src/hmr/devChannel.ts","../src/hmr/wrapHot.ts","../src/hmr/HmrManager.ts","../src/customId/routing.ts","../src/pagination/cursor.ts"],"sourcesContent":["import { GatedMetadataKey, InteractionRouteKeys } from '@src/metadataKeys';\n\nimport { discardCommits, runCheck, runCommits } from './effects';\n\nimport type { Gate, GateContextBase } from './Gate';\n\n// the stable id a route decorator stored, e.g. slash:daily or button:confirm. null for a plain event handler.\nexport function routeIdOf(handlerCtor: object): string | null {\n for (const [kind, key] of Object.entries(InteractionRouteKeys)) {\n // justified: getMetadata returns any, this key only ever stores the route/prefix string array\n const routes = Reflect.getMetadata(key, handlerCtor) as string[] | undefined;\n if (routes?.length) return `${kind}:${routes.join(',')}`;\n }\n return null;\n}\n\n/**\n * Runs each gate's check in order, so the first refusal propagates to the dispatcher boundary. An effect\n * gate's commit runs once the whole set passes.\n */\nexport async function runGates(gates: readonly Gate<GateContextBase>[], ctx: GateContextBase): Promise<void> {\n try {\n for (const gate of gates) {\n await runCheck(gate, ctx);\n }\n await runCommits(ctx);\n } finally {\n discardCommits(ctx);\n }\n}\n\n// dispatchers call this before execute, inside the boundary, so a refusal renders or drops\nexport async function runHandlerGates(handlerCtor: object, ctx: GateContextBase): Promise<void> {\n // justified: getMetadata returns any, and this key only ever stores the @Gated gate array\n const gates = Reflect.getMetadata(GatedMetadataKey, handlerCtor) as readonly Gate<GateContextBase>[] | undefined;\n if (!gates) return;\n await runGates(gates, { ...ctx, routeId: routeIdOf(handlerCtor) });\n}\n","import type { DevChannel, SeedcordCliEvents, SeedcordFrameworkEvents } from '@seedcord/types/internal';\n\n// one module instance per process, the HmrManager write and every framework read share it\nlet channel: DevChannel<SeedcordFrameworkEvents, SeedcordCliEvents> | undefined;\n\n/** @internal */\nexport function setDevChannel(next: DevChannel<SeedcordFrameworkEvents, SeedcordCliEvents> | undefined): void {\n channel = next;\n}\n\n/** @internal */\nexport function getDevChannel(): DevChannel<SeedcordFrameworkEvents, SeedcordCliEvents> | undefined {\n return channel;\n}\n","import type { DevChannel } from '@seedcord/types/internal';\n\n// the minimal raw-hot shape, satisfied by both vite's `import.meta.hot` and the CLI's `NormalizedHotChannel`.\ninterface RawHot {\n send(event: string, data?: unknown): void;\n on(event: string, cb: (data: unknown) => void): void;\n}\n\n/**\n * Wraps a raw vite hot object as a typed {@link DevChannel}.\n *\n * @internal\n */\nexport function wrapHot<TSend, TRecv>(hot: RawHot): DevChannel<TSend, TRecv> {\n return {\n send: (event, data) => hot.send(event, data),\n // justified: RawHot types every payload as unknown, the wire contract guarantees it matches TRecv[Key]\n on: (event, cb) => hot.on(event, cb as (data: unknown) => void)\n };\n}\n","// scoped here so import.meta.hot stays untyped in the rest of the package\n/// <reference types=\"vite/client\" />\nimport { Logger } from '@seedcord/logger';\nimport { formatFilePath } from '@seedcord/utils';\nimport chalk from 'chalk';\nimport { Envapter } from 'envapt';\n\nimport { setDevChannel } from './devChannel';\nimport { wrapHot } from './wrapHot';\n\nimport type { HmrAware, HmrUpdateEvent } from '@seedcord/types';\nimport type { SeedcordCliEvents, SeedcordFrameworkEvents } from '@seedcord/types/internal';\n\nexport class HmrManager {\n private readonly logger = new Logger('HMR', { channel: 'hmr' });\n private readonly listeners = new Set<HmrAware>();\n\n /** @internal */\n public init(): void {\n if (!import.meta.hot) return;\n // the only raw `import.meta.hot` read in the framework\n const channel = wrapHot<SeedcordFrameworkEvents, SeedcordCliEvents>(import.meta.hot);\n setDevChannel(channel);\n\n if (Envapter.isDevelopment || Envapter.isTest) {\n this.logger.info('Enabled');\n\n channel.on('seedcord:hmr', (payload) => {\n const affected = payload.affectedModules?.length ?? 0;\n this.logger.info(`${chalk.bold('1')} module changed, ${chalk.bold(affected)} affected modules`);\n\n if (payload.affectedModules) {\n this.logger.utils.list(\n payload.affectedModules.map((mod) => formatFilePath(mod)),\n 'Affected modules: '\n );\n }\n\n void this.handleUpdate(payload);\n });\n }\n }\n\n /** @internal */\n public register(listener: HmrAware): void {\n this.listeners.add(listener);\n }\n\n /** @internal */\n public unregister(listener: HmrAware): void {\n this.listeners.delete(listener);\n }\n\n private async handleUpdate(event: HmrUpdateEvent): Promise<void> {\n const promises = [...this.listeners].map(async (listener) => {\n try {\n await listener.onHmr(event);\n } catch (error) {\n this.logger.error(`Error handling HMR update in ${chalk.bold(listener.name)}: `, error);\n }\n });\n\n await Promise.all(promises);\n }\n}\n","import type { AnyCustomId } from './CustomId';\n\n/**\n * The route decorators store a handler's customId definitions here so the component base can decode\n * against them at runtime.\n *\n * @internal\n */\nexport const ComponentDefsKey = Symbol('seedcord:customId:componentDefs');\n\n/**\n * The phantom a component handler base carries. A route decorator constrains its argument to this, so\n * passing different definitions to the decorator and the handler's generic is a compile error. Never set at runtime.\n *\n * @internal\n */\nexport interface HasComponentDefs<Defs extends readonly AnyCustomId[]> {\n readonly __componentDefs?: Defs;\n}\n","import { CustomId } from '@customId/CustomId';\n\nimport type { CustomIdField } from '@customId/Field';\n\n// Included in the layout hash so it is fixed per prefix. Reducing it marks every active button as StaleCustomId.\n// @internal\nexport const PAGE_MAX = 1_000_000;\n\n// Discord rejects a message with duplicate custom_ids, and two controls can target the same page (first and\n// prev both hit 0), so each control uses a distinct slot.\n// @internal\nconst SLOT_MAX = 4;\n\n/** Page cursor for a paginator. */\nexport type PageCursor<Prefix extends string> = CustomId<\n Prefix,\n { page: CustomIdField<number>; slot: CustomIdField<number> }\n>;\n\n/** Build the page cursor for a paginator. */\nexport function pageCursor<Prefix extends string>(prefix: Prefix): PageCursor<Prefix> {\n return new CustomId(prefix).int('page', 0, PAGE_MAX).int('slot', 0, SLOT_MAX);\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,UAAU,aAAoC;CAC1D,KAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQ,oBAAoB,GAAG;EAE5D,MAAM,SAAS,QAAQ,YAAY,KAAK,WAAW;EACnD,IAAI,QAAQ,QAAQ,OAAO,GAAG,KAAK,GAAG,OAAO,KAAK,GAAG;CACzD;CACA,OAAO;AACX;;;;;AAMA,eAAsB,SAAS,OAAyC,KAAqC;CACzG,IAAI;EACA,KAAK,MAAM,QAAQ,OACf,MAAM,SAAS,MAAM,GAAG;EAE5B,MAAM,WAAW,GAAG;CACxB,UAAU;EACN,eAAe,GAAG;CACtB;AACJ;AAGA,eAAsB,gBAAgB,aAAqB,KAAqC;CAE5F,MAAM,QAAQ,QAAQ,YAAY,kBAAkB,WAAW;CAC/D,IAAI,CAAC,OAAO;CACZ,MAAM,SAAS,OAAO;EAAE,GAAG;EAAK,SAAS,UAAU,WAAW;CAAE,CAAC;AACrE;;;;AClCA,IAAI;;AAGJ,SAAgB,cAAc,MAAgF;CAC1G,UAAU;AACd;;AAGA,SAAgB,gBAAoF;CAChG,OAAO;AACX;;;;;;;;;ACAA,SAAgB,QAAsB,KAAuC;CACzE,OAAO;EACH,OAAO,OAAO,SAAS,IAAI,KAAK,OAAO,IAAI;EAE3C,KAAK,OAAO,OAAO,IAAI,GAAG,OAAO,EAA6B;CAClE;AACJ;;;;ACNA,IAAa,aAAb,MAAwB;CACpB,AAAiB,SAAS,IAAI,OAAO,OAAO,EAAE,SAAS,MAAM,CAAC;CAC9D,AAAiB,4BAAY,IAAI,IAAc;;CAG/C,AAAO,OAAa;EAChB,IAAI,CAAC,OAAO,KAAK,KAAK;EAEtB,MAAM,UAAU,QAAoD,OAAO,KAAK,GAAG;EACnF,cAAc,OAAO;EAErB,IAAI,SAAS,iBAAiB,SAAS,QAAQ;GAC3C,KAAK,OAAO,KAAK,SAAS;GAE1B,QAAQ,GAAG,iBAAiB,YAAY;IACpC,MAAM,WAAW,QAAQ,iBAAiB,UAAU;IACpD,KAAK,OAAO,KAAK,GAAG,MAAM,KAAK,GAAG,EAAE,mBAAmB,MAAM,KAAK,QAAQ,EAAE,kBAAkB;IAE9F,IAAI,QAAQ,iBACR,KAAK,OAAO,MAAM,KACd,QAAQ,gBAAgB,KAAK,QAAQ,eAAe,GAAG,CAAC,GACxD,oBACJ;IAGJ,AAAK,KAAK,aAAa,OAAO;GAClC,CAAC;EACL;CACJ;;CAGA,AAAO,SAAS,UAA0B;EACtC,KAAK,UAAU,IAAI,QAAQ;CAC/B;;CAGA,AAAO,WAAW,UAA0B;EACxC,KAAK,UAAU,OAAO,QAAQ;CAClC;CAEA,MAAc,aAAa,OAAsC;EAC7D,MAAM,WAAW,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,IAAI,OAAO,aAAa;GACzD,IAAI;IACA,MAAM,SAAS,MAAM,KAAK;GAC9B,SAAS,OAAO;IACZ,KAAK,OAAO,MAAM,gCAAgC,MAAM,KAAK,SAAS,IAAI,EAAE,KAAK,KAAK;GAC1F;EACJ,CAAC;EAED,MAAM,QAAQ,IAAI,QAAQ;CAC9B;AACJ;;;;;;;;;;ACxDA,MAAa,mBAAmB,OAAO,iCAAiC;;;;ACFxE,MAAa,WAAW;AAKxB,MAAM,WAAW;;AASjB,SAAgB,WAAkC,QAAoC;CAClF,OAAO,IAAI,SAAS,MAAM,CAAC,CAAC,IAAI,QAAQ,GAAG,QAAQ,CAAC,CAAC,IAAI,QAAQ,GAAG,QAAQ;AAChF"}
@@ -0,0 +1,34 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+
29
+ Object.defineProperty(exports, '__toESM', {
30
+ enumerable: true,
31
+ get: function () {
32
+ return __toESM;
33
+ }
34
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@seedcord/core",
3
3
  "type": "module",
4
- "version": "0.1.0-next.3",
4
+ "version": "0.1.0-next.4",
5
5
  "description": "Transport-agnostic core for Seedcord bots",
6
6
  "repository": {
7
7
  "type": "git",
@@ -29,6 +29,16 @@
29
29
  "types": "./dist/internal.index.d.cts",
30
30
  "default": "./dist/internal.index.cjs"
31
31
  }
32
+ },
33
+ "./hmr": {
34
+ "import": {
35
+ "types": "./dist/hmr.index.d.mts",
36
+ "default": "./dist/hmr.index.mjs"
37
+ },
38
+ "require": {
39
+ "types": "./dist/hmr.index.d.cts",
40
+ "default": "./dist/hmr.index.cjs"
41
+ }
32
42
  }
33
43
  },
34
44
  "files": [
@@ -43,16 +53,20 @@
43
53
  },
44
54
  "dependencies": {
45
55
  "@discordjs/builders": "^1.14.1",
56
+ "chalk": "^5.6.2",
46
57
  "discord-api-types": "^0.38.49",
58
+ "envapt": "^8.0.0",
47
59
  "@seedcord/errors": "0.3.0-next.3",
48
- "@seedcord/types": "0.8.0-next.4",
49
- "@seedcord/utils": "0.8.0-next.4"
60
+ "@seedcord/logger": "0.1.0-next.0",
61
+ "@seedcord/types": "0.8.0-next.5",
62
+ "@seedcord/utils": "0.8.0-next.5"
50
63
  },
51
64
  "devDependencies": {
52
65
  "reflect-metadata": "^0.2.2",
53
66
  "type-fest": "^5.7.0",
67
+ "vite": "^8.0.16",
54
68
  "@seedcord/eslint-config": "1.5.0-next.3",
55
- "@seedcord/rate-limiter": "0.1.0-next.1",
69
+ "@seedcord/rate-limiter": "0.1.0-next.2",
56
70
  "@seedcord/tsconfig": "2.0.1",
57
71
  "@seedcord/tsdown-config": "2.0.1"
58
72
  },