zumito-framework 1.22.2 → 1.23.0

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.
@@ -316,27 +316,44 @@ export class ZumitoFramework {
316
316
  modulesFolder = `${process.cwd()}/src/modules`;
317
317
  }
318
318
  const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
319
+ const moduleEntries = [];
319
320
  if (this.settings.bundles && this.settings.bundles.length > 0) {
320
321
  for (const bundle of this.settings.bundles) {
321
- await this.registerBundle(bundle.path, bundle.options);
322
+ moduleEntries.push({
323
+ rootPath: bundle.path,
324
+ options: bundle.options,
325
+ });
322
326
  }
323
327
  }
324
- await this.registerModule(path.join(__dirname, 'modules', 'core'), 'baseModule');
328
+ moduleEntries.push({
329
+ rootPath: path.join(__dirname, 'modules', 'core'),
330
+ name: 'baseModule',
331
+ });
325
332
  if (fs.existsSync(`${process.cwd()}/node_modules/.zumitoBundles`)) {
326
333
  const files = fs.readdirSync(`${process.cwd()}/node_modules/.zumitoBundles`);
327
334
  for (const file of files) {
328
- await this.registerModule(`${process.cwd()}/node_modules/.zumitoBundles`, file);
335
+ moduleEntries.push({
336
+ rootPath: path.join(process.cwd(), 'node_modules', '.zumitoBundles', file),
337
+ name: file,
338
+ });
329
339
  }
330
340
  }
331
341
  if (modulesFolder) {
332
342
  const files = fs.readdirSync(modulesFolder);
333
343
  for (const file of files) {
334
- await this.registerModule(modulesFolder, file);
344
+ moduleEntries.push({
345
+ rootPath: path.join(modulesFolder, file),
346
+ name: file,
347
+ });
335
348
  }
336
349
  }
337
350
  else if (this.settings.srcMode == 'monoBundle') {
338
- await this.registerModule(process.cwd(), 'src');
351
+ moduleEntries.push({
352
+ rootPath: path.join(process.cwd(), 'src'),
353
+ name: 'src',
354
+ });
339
355
  }
356
+ await this.modules.resolveAndInstantiateAll(moduleEntries);
340
357
  }
341
358
  async registerModule(modulesFolder, moduleName, module) {
342
359
  if (!module) {
@@ -8,11 +8,22 @@ export type ModuleRequeriments = {
8
8
  services: Array<string>;
9
9
  custom: Array<() => Promise<boolean>>;
10
10
  };
11
+ export type ModuleDeclaration = {
12
+ moduleClass: typeof Module;
13
+ name: string;
14
+ rootPath: string;
15
+ options?: ModuleParameters;
16
+ requiredDeps: string[];
17
+ optionalDeps: string[];
18
+ };
11
19
  export declare abstract class Module {
12
20
  protected path: string;
13
21
  protected parameters: ModuleParameters;
14
22
  protected framework: ZumitoFramework;
15
23
  protected events: Map<string, FrameworkEvent>;
24
+ static moduleName?: string;
25
+ static dependencies?: readonly string[];
26
+ static optionalDependencies?: readonly string[];
16
27
  static requeriments: ModuleRequeriments;
17
28
  protected errorHandler: ErrorHandler;
18
29
  constructor(path: any, parameters?: ModuleParameters);
@@ -11,6 +11,9 @@ export class Module {
11
11
  parameters;
12
12
  framework;
13
13
  events = new Map();
14
+ static moduleName;
15
+ static dependencies;
16
+ static optionalDependencies;
14
17
  static requeriments;
15
18
  errorHandler;
16
19
  constructor(path, parameters) {
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ import { DatabaseConfigLoader } from './services/utilities/DatabaseConfigLoader.
12
12
  import { EmojiFallback } from './services/EmojiFallback.js';
13
13
  import { FrameworkEvent } from './definitions/FrameworkEvent.js';
14
14
  import { FrameworkSettings } from './definitions/FrameworkSettings.js';
15
- import { Module } from './definitions/Module.js';
15
+ import { Module, ModuleDeclaration, ModuleRequeriments } from './definitions/Module.js';
16
16
  import { SelectMenuParameters } from './definitions/parameters/SelectMenuParameters.js';
17
17
  import { TextFormatter } from './services/utilities/TextFormatter.js';
18
18
  import { Translation } from './definitions/Translation.js';
@@ -42,4 +42,4 @@ export { CommandBinds } from './definitions/commands/CommandBinds.js';
42
42
  export { Injectable } from './definitions/decorators/Injectable.decorator.js';
43
43
  export { LauncherConfig } from './definitions/config/LauncherConfig.js';
44
44
  export { ModuleParameters } from './definitions/parameters/ModuleParameters.js';
45
- export { ZumitoFramework, FrameworkSettings, Command, Module, CommandParameters, CommandArguments, FrameworkEvent, Translation, TranslationManager, ApiResponse, SelectMenuParameters, CommandType, CommandArgDefinition, CommandChoiceDefinition, ButtonPressed, ButtonPressedParams, TextFormatter, EmojiFallback, DatabaseConfigLoader, PresenceDataRule, RuledPresenceData, StatusManagerOptions, discord, EventParameters, ServiceContainer, GuildDataGetter, SlashCommandRefresher, CommandParser, ErrorHandler, ErrorType, Route, RouteMethod, InteractionHandler, CommandManager, InviteUrlGenerator, PrefixResolver, CommandExecutionChecker, DatabaseManager, };
45
+ export { ZumitoFramework, FrameworkSettings, Command, Module, ModuleDeclaration, ModuleRequeriments, CommandParameters, CommandArguments, FrameworkEvent, Translation, TranslationManager, ApiResponse, SelectMenuParameters, CommandType, CommandArgDefinition, CommandChoiceDefinition, ButtonPressed, ButtonPressedParams, TextFormatter, EmojiFallback, DatabaseConfigLoader, PresenceDataRule, RuledPresenceData, StatusManagerOptions, discord, EventParameters, ServiceContainer, GuildDataGetter, SlashCommandRefresher, CommandParser, ErrorHandler, ErrorType, Route, RouteMethod, InteractionHandler, CommandManager, InviteUrlGenerator, PrefixResolver, CommandExecutionChecker, DatabaseManager, };
@@ -76,6 +76,7 @@ export class MessageCreate extends FrameworkEvent {
76
76
  allowedMentions: { repliedUser: false },
77
77
  });
78
78
  }
79
+ const startTime = Date.now();
79
80
  await commandInstance.execute({
80
81
  message,
81
82
  args: parsedArgs,
@@ -90,7 +91,22 @@ export class MessageCreate extends FrameworkEvent {
90
91
  return this.framework.translations.get('command.' + commandInstance.name + '.' + key, guildSettings.lang, params);
91
92
  }
92
93
  },
94
+ }).then(() => {
95
+ this.framework.eventEmitter.emit('commandExecuted', {
96
+ guildId: message.guildId,
97
+ commandName: commandInstance.name,
98
+ type: 'prefix',
99
+ executionTimeMs: Date.now() - startTime,
100
+ success: true,
101
+ });
93
102
  }).catch((error) => {
103
+ this.framework.eventEmitter.emit('commandExecuted', {
104
+ guildId: message.guildId,
105
+ commandName: commandInstance.name,
106
+ type: 'prefix',
107
+ executionTimeMs: Date.now() - startTime,
108
+ success: false,
109
+ });
94
110
  const errorHandler = ServiceContainer.getService(ErrorHandler);
95
111
  errorHandler.handleError(error, {
96
112
  command: commandInstance,
@@ -112,31 +112,57 @@ export class InteractionHandler {
112
112
  return;
113
113
  }
114
114
  const trans = this.translationManager.getShortHandMethod('command.' + commandInstance.name, guildSettings?.lang);
115
+ const startTime = Date.now();
115
116
  if (commandInstance.type === CommandType.separated ||
116
117
  commandInstance.type === CommandType.slash) {
117
- await commandInstance.executeSlashCommand({
118
- client: this.client,
119
- interaction, args, framework, guildSettings, trans,
120
- });
121
- }
122
- else {
123
118
  try {
124
- await commandInstance.execute({
119
+ await commandInstance.executeSlashCommand({
125
120
  client: this.client,
126
121
  interaction, args, framework, guildSettings, trans,
127
- }).catch((error) => {
128
- this.errorHandler.handleError(error, {
129
- command: commandInstance,
130
- type: ErrorType.CommandRun,
131
- interaction,
132
- });
133
- interaction.reply({
134
- content: "An error ocurred while running this command.",
135
- ephemeral: true
136
- });
122
+ });
123
+ framework.eventEmitter.emit('commandExecuted', {
124
+ guildId: interaction.guildId,
125
+ commandName: commandInstance.name,
126
+ type: 'slash',
127
+ executionTimeMs: Date.now() - startTime,
128
+ success: true,
137
129
  });
138
130
  }
139
131
  catch (error) {
132
+ framework.eventEmitter.emit('commandExecuted', {
133
+ guildId: interaction.guildId,
134
+ commandName: commandInstance.name,
135
+ type: 'slash',
136
+ executionTimeMs: Date.now() - startTime,
137
+ success: false,
138
+ });
139
+ this.errorHandler.handleError(error, {
140
+ command: commandInstance,
141
+ type: ErrorType.CommandRun,
142
+ interaction,
143
+ });
144
+ }
145
+ }
146
+ else {
147
+ await commandInstance.execute({
148
+ client: this.client,
149
+ interaction, args, framework, guildSettings, trans,
150
+ }).then(() => {
151
+ framework.eventEmitter.emit('commandExecuted', {
152
+ guildId: interaction.guildId,
153
+ commandName: commandInstance.name,
154
+ type: 'slash',
155
+ executionTimeMs: Date.now() - startTime,
156
+ success: true,
157
+ });
158
+ }).catch((error) => {
159
+ framework.eventEmitter.emit('commandExecuted', {
160
+ guildId: interaction.guildId,
161
+ commandName: commandInstance.name,
162
+ type: 'slash',
163
+ executionTimeMs: Date.now() - startTime,
164
+ success: false,
165
+ });
140
166
  this.errorHandler.handleError(error, {
141
167
  command: commandInstance,
142
168
  type: ErrorType.CommandRun,
@@ -144,9 +170,9 @@ export class InteractionHandler {
144
170
  });
145
171
  interaction.reply({
146
172
  content: "An error ocurred while running this command.",
147
- ephemeral: true,
173
+ ephemeral: true
148
174
  });
149
- }
175
+ });
150
176
  }
151
177
  }
152
178
  async handleButtonInteraction(interaction, guildSettings) {
@@ -1,5 +1,5 @@
1
1
  import { ZumitoFramework } from "../../ZumitoFramework.js";
2
- import { Module } from "../../definitions/Module.js";
2
+ import { Module, ModuleDeclaration } from "../../definitions/Module.js";
3
3
  import { ErrorHandler } from "../handlers/ErrorHandler.js";
4
4
  import { ModuleParameters } from "../../definitions/parameters/ModuleParameters.js";
5
5
  export declare class ModuleManager {
@@ -20,7 +20,19 @@ export declare class ModuleManager {
20
20
  * @deprecated
21
21
  */
22
22
  get size(): number;
23
- loadModuleFile(folderPath: string): Promise<unknown>;
23
+ resolveModuleName(moduleClass: typeof Module, fallbackName: string): string;
24
+ resolveModuleDeps(moduleClass: typeof Module): {
25
+ required: string[];
26
+ optional: string[];
27
+ };
28
+ buildDependencyGraph(declarations: ModuleDeclaration[]): Map<string, string[]>;
29
+ topologicalSort(declarations: ModuleDeclaration[]): ModuleDeclaration[];
30
+ resolveAndInstantiateAll(modulePaths: Array<{
31
+ rootPath: string;
32
+ options?: ModuleParameters;
33
+ name?: string;
34
+ }>): Promise<void>;
35
+ loadModuleFile(folderPath: string): Promise<typeof Module | undefined>;
24
36
  registerModule(module: InstanceType<typeof Module>): void;
25
37
  instanceModule(module: any, rootPath: string, name?: string, options?: ModuleParameters): Promise<Module | {
26
38
  modules: string[];
@@ -30,6 +30,123 @@ export class ModuleManager {
30
30
  get size() {
31
31
  return this.modules.size;
32
32
  }
33
+ resolveModuleName(moduleClass, fallbackName) {
34
+ return moduleClass.moduleName || fallbackName;
35
+ }
36
+ resolveModuleDeps(moduleClass) {
37
+ const required = new Set();
38
+ const optional = new Set();
39
+ if (moduleClass.dependencies) {
40
+ for (const d of moduleClass.dependencies)
41
+ required.add(d);
42
+ }
43
+ if (moduleClass.optionalDependencies) {
44
+ for (const d of moduleClass.optionalDependencies)
45
+ optional.add(d);
46
+ }
47
+ if (moduleClass.requeriments?.modules) {
48
+ for (const m of moduleClass.requeriments.modules)
49
+ required.add(m);
50
+ }
51
+ return { required: [...required], optional: [...optional] };
52
+ }
53
+ buildDependencyGraph(declarations) {
54
+ const nameSet = new Set(declarations.map((d) => d.name));
55
+ const graph = new Map();
56
+ for (const decl of declarations) {
57
+ const deps = [...decl.requiredDeps];
58
+ for (const opt of decl.optionalDeps) {
59
+ if (nameSet.has(opt))
60
+ deps.push(opt);
61
+ }
62
+ for (const dep of deps) {
63
+ if (!nameSet.has(dep)) {
64
+ throw new Error(`Module "${decl.name}" depends on "${dep}" which was not found`);
65
+ }
66
+ }
67
+ graph.set(decl.name, deps);
68
+ }
69
+ return graph;
70
+ }
71
+ topologicalSort(declarations) {
72
+ const graph = this.buildDependencyGraph(declarations);
73
+ const inDegree = new Map();
74
+ const adjacency = new Map();
75
+ for (const decl of declarations) {
76
+ const name = decl.name;
77
+ if (!inDegree.has(name))
78
+ inDegree.set(name, 0);
79
+ if (!adjacency.has(name))
80
+ adjacency.set(name, []);
81
+ }
82
+ for (const [name, deps] of graph) {
83
+ for (const dep of deps) {
84
+ if (!adjacency.has(dep))
85
+ adjacency.set(dep, []);
86
+ adjacency.get(dep).push(name);
87
+ inDegree.set(name, (inDegree.get(name) || 0) + 1);
88
+ }
89
+ }
90
+ const queue = [];
91
+ for (const [name, degree] of inDegree) {
92
+ if (degree === 0)
93
+ queue.push(name);
94
+ }
95
+ const sorted = [];
96
+ while (queue.length > 0) {
97
+ const current = queue.shift();
98
+ sorted.push(current);
99
+ for (const neighbor of adjacency.get(current) || []) {
100
+ const newDegree = inDegree.get(neighbor) - 1;
101
+ inDegree.set(neighbor, newDegree);
102
+ if (newDegree === 0)
103
+ queue.push(neighbor);
104
+ }
105
+ }
106
+ if (sorted.length < declarations.length) {
107
+ const unresolved = declarations
108
+ .filter((d) => !sorted.includes(d.name))
109
+ .map((d) => d.name);
110
+ throw new Error(`Circular dependency detected involving: ${unresolved.join(', ')}`);
111
+ }
112
+ const nameMap = new Map(declarations.map((d) => [d.name, d]));
113
+ return sorted.map((name) => nameMap.get(name));
114
+ }
115
+ async resolveAndInstantiateAll(modulePaths) {
116
+ const declarations = [];
117
+ for (const entry of modulePaths) {
118
+ const moduleClass = await this.loadModuleFile(entry.rootPath);
119
+ if (!moduleClass || moduleClass === Module)
120
+ continue;
121
+ const { required, optional } = this.resolveModuleDeps(moduleClass);
122
+ const name = this.resolveModuleName(moduleClass, entry.name || path.basename(entry.rootPath));
123
+ declarations.push({
124
+ moduleClass,
125
+ name,
126
+ rootPath: entry.rootPath,
127
+ options: entry.options,
128
+ requiredDeps: required,
129
+ optionalDeps: optional,
130
+ });
131
+ }
132
+ if (declarations.length === 0)
133
+ return;
134
+ let sorted;
135
+ try {
136
+ sorted = this.topologicalSort(declarations);
137
+ }
138
+ catch (err) {
139
+ console.error(`[📦❌] ${err.message}`);
140
+ return;
141
+ }
142
+ for (const decl of sorted) {
143
+ const result = await this.instanceModule(decl.moduleClass, decl.rootPath, decl.name, decl.options);
144
+ if (result instanceof Module) {
145
+ console.log(`[📦✅] Module "${decl.name}" loaded successfully`);
146
+ }
147
+ }
148
+ await this.initializePendingModules();
149
+ }
33
150
  async loadModuleFile(folderPath) {
34
151
  let file;
35
152
  if (fs.existsSync(path.join(folderPath, 'index.js'))) {
@@ -46,7 +163,7 @@ export class ModuleManager {
46
163
  const moduleClass = exports.find((candidate) => {
47
164
  return Object.getPrototypeOf(candidate).name === 'Module';
48
165
  });
49
- return moduleClass || exports[0];
166
+ return (moduleClass || exports[0]);
50
167
  }
51
168
  registerModule(module) {
52
169
  // Register module events
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zumito-framework",
3
- "version": "1.22.2",
3
+ "version": "1.23.0",
4
4
  "description": "Discord.js bot framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",