@yumerijs/loader 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,27 +1,52 @@
1
- import { Core, Config, Logger, Context } from '@yumerijs/core';
1
+ import { Core, Config, Logger, Context, PluginStatus } from '@yumerijs/core';
2
2
  interface Plugin {
3
3
  apply: (ctx: Context, config: Config) => Promise<void>;
4
4
  disable: (ctx: Context) => Promise<void>;
5
5
  depend: Array<string>;
6
6
  provide: Array<string>;
7
7
  }
8
- interface PluginLoader {
9
- load(pluginName: string): Promise<Plugin>;
10
- unloadPlugin(pluginName: string): Promise<void>;
11
- checkPluginDependencies(pluginPath: string): Promise<boolean>;
12
- installPluginDependencies(pluginName: string): Promise<void>;
13
- logger: Logger;
14
- }
15
- declare class PluginLoader {
16
- private pluginCache;
17
- private core;
18
- private config;
19
- private isDev;
8
+ export declare class PluginLoader {
20
9
  private pluginsDir;
10
+ core: Core;
11
+ config: any;
21
12
  logger: Logger;
22
- constructor(core?: Core, config?: Config, pluginsDir?: string);
23
- setCoreAndConfig(core: Core, config: Config): void;
24
- private loadPluginFromPath;
13
+ plugins: {
14
+ [name: string]: Plugin & {
15
+ depend?: string[];
16
+ provide?: string[];
17
+ };
18
+ };
19
+ pluginStatus: Record<string, PluginStatus>;
20
+ private pluginWatchers;
21
+ private pluginModules;
22
+ private configPath;
23
+ private pluginContexts;
24
+ private isDev;
25
+ constructor(core?: Core, pluginsDir?: string);
26
+ /**
27
+ * Reloads the config file from disk into memory and emits a 'config-reloaded' event.
28
+ * This does NOT reload any plugins.
29
+ */
30
+ reloadConfigFile(): Promise<void>;
31
+ getCore(): Core;
32
+ getContext(pluginName: string): Context;
33
+ unregall(pluginName: string): void;
34
+ loadConfig(configPath: string): Promise<void>;
35
+ getPluginConfig(pluginName: string): Promise<Config>;
36
+ loadPlugins(): Promise<void>;
37
+ loadSinglePlugin(pluginName: string, triggerPendingCheck?: boolean, onlypending?: boolean): Promise<boolean>;
38
+ private _loadPendingPlugins;
39
+ unloadPlugin(pluginNameToUnload: string, ispending?: boolean): Promise<void>;
40
+ private _unloadSinglePlugin;
41
+ /**
42
+ * Reloads a single plugin's code by clearing the require cache and then reloading it.
43
+ * @param pluginName The name of the plugin to reload.
44
+ */
45
+ reloadPlugin(pluginName: string): Promise<void>;
46
+ private watchPlugin;
47
+ loadModule(pluginName: string): Promise<Plugin>;
25
48
  private clearRequireCache;
49
+ checkPluginDependencies(pluginPath: string): Promise<boolean>;
50
+ installPluginDependencies(pluginName: string): Promise<void>;
26
51
  }
27
52
  export default PluginLoader;
package/dist/index.js CHANGED
@@ -33,147 +33,310 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PluginLoader = void 0;
36
37
  const path = __importStar(require("path"));
37
38
  const core_1 = require("@yumerijs/core");
38
39
  const fs = __importStar(require("fs"));
39
40
  const util_1 = require("util");
40
41
  const child_process_1 = require("child_process");
41
- const url_1 = require("url");
42
+ const yaml = __importStar(require("js-yaml"));
43
+ const chokidar = __importStar(require("chokidar"));
42
44
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
43
45
  class PluginLoader {
44
- pluginCache = {};
45
- core = null;
46
+ pluginsDir;
47
+ core;
46
48
  config = null;
47
- isDev = false;
48
- pluginsDir = 'plugins'; // Default plugins directory
49
49
  logger = new core_1.Logger('loader');
50
- constructor(core, config, pluginsDir = 'plugins') {
51
- this.pluginCache = {};
52
- this.core = core || null;
53
- this.config = config || null;
54
- this.isDev = process.env.NODE_ENV === 'development';
50
+ plugins = {};
51
+ pluginStatus = {};
52
+ pluginWatchers = {};
53
+ pluginModules = {};
54
+ configPath = '';
55
+ pluginContexts = {};
56
+ isDev = false;
57
+ constructor(core, pluginsDir = 'plugins') {
55
58
  this.pluginsDir = pluginsDir;
59
+ this.core = core || new core_1.Core(this, undefined, false);
60
+ this.core.uuid = Math.random().toString(36).substring(2);
61
+ this.logger.info(`[DIAG] Loader created/received Core instance with UUID: ${this.core.uuid}`);
62
+ this.isDev = process.env.NODE_ENV === 'development';
63
+ core_1.Logger.setCore(this.core);
64
+ }
65
+ /**
66
+ * Reloads the config file from disk into memory and emits a 'config-reloaded' event.
67
+ * This does NOT reload any plugins.
68
+ */
69
+ async reloadConfigFile() {
70
+ this.logger.info('Reloading config file...');
71
+ try {
72
+ const doc = yaml.load(fs.readFileSync(this.configPath, 'utf8'));
73
+ this.config = doc;
74
+ this.core.coreConfig = this.config.core || {};
75
+ this.core.emit('config-reloaded', this.config);
76
+ this.logger.info('Config file reloaded.');
77
+ }
78
+ catch (e) {
79
+ this.logger.error('Failed to reload config file:', e);
80
+ }
56
81
  }
57
- setCoreAndConfig(core, config) {
58
- this.core = core;
59
- this.config = config;
82
+ getCore() {
83
+ return this.core;
60
84
  }
61
- async load(pluginName) {
62
- if (this.pluginCache[pluginName] && !this.isDev) {
63
- return this.pluginCache[pluginName];
85
+ getContext(pluginName) {
86
+ if (!this.pluginContexts[pluginName]) {
87
+ this.pluginContexts[pluginName] = new core_1.Context(this.core, pluginName);
64
88
  }
65
- let pluginPath;
66
- let isLocalPlugin = false;
67
- const localPluginPath = path.resolve(process.cwd(), this.pluginsDir, pluginName);
68
- const localPluginPathAlt = path.resolve(process.cwd(), pluginName);
69
- if (fs.existsSync(localPluginPath)) {
70
- pluginPath = localPluginPath;
71
- isLocalPlugin = true;
89
+ return this.pluginContexts[pluginName];
90
+ }
91
+ unregall(pluginName) {
92
+ const ctx = this.pluginContexts[pluginName];
93
+ if (ctx) {
94
+ ctx.dispose();
95
+ delete this.pluginContexts[pluginName];
72
96
  }
73
- else if (fs.existsSync(localPluginPathAlt)) {
74
- pluginPath = localPluginPathAlt;
75
- isLocalPlugin = true;
97
+ }
98
+ async loadConfig(configPath) {
99
+ try {
100
+ this.configPath = configPath;
101
+ const doc = yaml.load(fs.readFileSync(configPath, 'utf8'));
102
+ this.config = doc;
103
+ this.logger.info('Config loaded.');
104
+ this.core.coreConfig = this.config.core || {};
105
+ this.core.i18n = new (require('@yumerijs/core').I18n)(this.core.coreConfig.lang || ['zh', 'en']);
76
106
  }
77
- else if (path.isAbsolute(pluginName) || pluginName.startsWith('.')) {
78
- pluginPath = path.resolve(pluginName);
79
- isLocalPlugin = true;
107
+ catch (e) {
108
+ this.logger.error('Failed to load config:', e);
109
+ throw e;
80
110
  }
81
- else {
82
- try {
83
- pluginPath = require.resolve(pluginName);
111
+ }
112
+ async getPluginConfig(pluginName) {
113
+ const actualPluginName = pluginName.startsWith('~') ? pluginName.substring(1) : pluginName;
114
+ if (!this.config.plugins[actualPluginName]) {
115
+ return new core_1.Config(actualPluginName);
116
+ }
117
+ return new core_1.Config(actualPluginName, this.config.plugins[actualPluginName]);
118
+ }
119
+ async loadPlugins() {
120
+ if (!this.config || typeof this.config.plugins !== 'object' || this.config.plugins === null) {
121
+ this.logger.info('No plugins configuration found. No plugins to load.');
122
+ return;
123
+ }
124
+ const allPluginNames = Object.keys(this.config.plugins);
125
+ this.pluginStatus = {}; // Reset status
126
+ for (const name of allPluginNames) {
127
+ if (name.startsWith('~')) {
128
+ const actualName = name.substring(1);
129
+ this.pluginStatus[actualName] = "disabled" /* PluginStatus.DISABLED */;
84
130
  }
85
- catch (e) {
86
- if (e.code === 'MODULE_NOT_FOUND') {
87
- throw new Error(`Plugin ${pluginName} not found. Please install it first.`);
88
- }
89
- throw e;
131
+ else {
132
+ this.pluginStatus[name] = "pending" /* PluginStatus.PENDING */;
90
133
  }
91
134
  }
92
- try {
93
- if (isLocalPlugin) {
94
- const plugin = await this.loadPluginFromPath(pluginPath, isLocalPlugin);
95
- this.pluginCache[pluginName] = plugin;
96
- return plugin;
135
+ const enabledPlugins = allPluginNames.filter(name => !name.startsWith('~'));
136
+ const currentlyLoaded = Object.keys(this.plugins);
137
+ for (const loadedName of currentlyLoaded) {
138
+ if (!enabledPlugins.includes(loadedName)) {
139
+ await this.unloadPlugin(loadedName);
97
140
  }
98
- else {
99
- const plugin = await Promise.resolve(`${pluginName}`).then(s => __importStar(require(s)));
100
- return plugin.default || plugin;
141
+ }
142
+ if (enabledPlugins.length === 0) {
143
+ this.logger.info('No enabled plugins found in configuration.');
144
+ return;
145
+ }
146
+ let loadedInLastPass = true;
147
+ while (loadedInLastPass) {
148
+ loadedInLastPass = false;
149
+ for (const pluginName of enabledPlugins) {
150
+ if (this.pluginStatus[pluginName] === "enabled" /* PluginStatus.ENABLED */) {
151
+ continue;
152
+ }
153
+ const success = await this.loadSinglePlugin(pluginName, false);
154
+ if (success) {
155
+ loadedInLastPass = true;
156
+ }
101
157
  }
102
158
  }
103
- catch (e) {
104
- this.logger.error(`Failed to load plugin from ${pluginPath}:`, e);
105
- throw e;
159
+ await this._loadPendingPlugins();
160
+ const pendingPlugins = Object.keys(this.pluginStatus).filter(p => this.pluginStatus[p] === "pending" /* PluginStatus.PENDING */);
161
+ if (pendingPlugins.length > 0) {
162
+ // this.logger.warn('Some plugins could not be loaded due to unresolved dependencies:', pendingPlugins);
106
163
  }
107
164
  }
108
- async loadPluginFromPath(pluginPath, isLocalPlugin) {
165
+ async loadSinglePlugin(pluginName, triggerPendingCheck = true, onlypending = false) {
166
+ if (!this.pluginStatus[pluginName]) {
167
+ this.pluginStatus[pluginName] = "pending" /* PluginStatus.PENDING */;
168
+ }
169
+ if (this.pluginStatus[pluginName] !== "pending" /* PluginStatus.PENDING */ && onlypending) {
170
+ return false;
171
+ }
109
172
  try {
110
- let targetPath = pluginPath;
111
- if (isLocalPlugin && this.isDev) {
112
- const jsonPath = path.join(pluginPath, 'package.json');
113
- if (fs.existsSync(jsonPath)) {
114
- const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
115
- const pkg = JSON.parse(jsonContent);
116
- if (pkg.dev) {
117
- targetPath = path.join(pluginPath, pkg.dev);
173
+ const pluginInstance = await this.loadModule(pluginName);
174
+ if (!pluginInstance) {
175
+ throw new Error('Plugin loader returned no instance.');
176
+ }
177
+ const deps = pluginInstance.depend || [];
178
+ const unmetDependencies = deps.filter(dep => !this.core.components[dep]);
179
+ if (unmetDependencies.length > 0) {
180
+ return false;
181
+ }
182
+ this.plugins[pluginName] = pluginInstance;
183
+ const pluginConfig = await this.getPluginConfig(pluginName);
184
+ const context = this.getContext(pluginName);
185
+ await this.core.plugin(pluginInstance, context, pluginConfig);
186
+ this.pluginStatus[pluginName] = "enabled" /* PluginStatus.ENABLED */;
187
+ if (triggerPendingCheck) {
188
+ await this._loadPendingPlugins();
189
+ }
190
+ if (this.isDev) {
191
+ let pluginPathToWatch = null;
192
+ try {
193
+ const pkgJsonPath = require.resolve(`${pluginName}/package.json`);
194
+ pluginPathToWatch = path.dirname(pkgJsonPath);
195
+ }
196
+ catch (e) {
197
+ const localPluginPath = path.resolve(process.cwd(), pluginName);
198
+ const localPluginPathInPlugins = path.resolve(process.cwd(), 'plugins', pluginName);
199
+ if (fs.existsSync(localPluginPath)) {
200
+ pluginPathToWatch = localPluginPath;
118
201
  }
119
- else if (pkg.main) {
120
- targetPath = path.join(pluginPath, pkg.main);
202
+ else if (fs.existsSync(localPluginPathInPlugins)) {
203
+ pluginPathToWatch = localPluginPathInPlugins;
121
204
  }
122
205
  }
206
+ if (pluginPathToWatch) {
207
+ this.watchPlugin(pluginName, pluginPathToWatch);
208
+ }
209
+ else {
210
+ this.logger.warn(`Could not resolve path for plugin ${pluginName} to watch for changes.`);
211
+ }
123
212
  }
124
- if (this.isDev) {
125
- // In development, use require() to leverage esbuild-register
126
- // and clear cache for HMR.
127
- const resolvedPath = require.resolve(targetPath);
128
- // Clean up the cache for the main module and its children
129
- if (require.cache[resolvedPath]) {
130
- this.clearRequireCache(resolvedPath, new Set());
213
+ return true;
214
+ }
215
+ catch (err) {
216
+ this.logger.error(`Failed to load plugin "${pluginName}":`, err);
217
+ return false;
218
+ }
219
+ }
220
+ async _loadPendingPlugins() {
221
+ const pendingPlugins = Object.keys(this.pluginStatus).filter(p => this.pluginStatus[p] === "pending" /* PluginStatus.PENDING */);
222
+ if (pendingPlugins.length === 0)
223
+ return;
224
+ for (const pluginName of pendingPlugins) {
225
+ await this.loadSinglePlugin(pluginName, false);
226
+ }
227
+ }
228
+ async unloadPlugin(pluginNameToUnload, ispending = false) {
229
+ const dependents = [];
230
+ const pluginsToCheck = [pluginNameToUnload];
231
+ while (pluginsToCheck.length > 0) {
232
+ const currentPluginName = pluginsToCheck.shift();
233
+ const provided = this.plugins[currentPluginName]?.provide || [];
234
+ if (provided.length === 0)
235
+ continue;
236
+ for (const pluginName in this.plugins) {
237
+ if (dependents.includes(pluginName) || pluginName === pluginNameToUnload)
238
+ continue;
239
+ const deps = this.plugins[pluginName].depend || [];
240
+ if (provided.some(p => deps.includes(p))) {
241
+ if (!dependents.includes(pluginName)) {
242
+ dependents.push(pluginName);
243
+ pluginsToCheck.push(pluginName);
244
+ }
131
245
  }
132
- const module = require(resolvedPath);
133
- return module.default || module;
134
246
  }
135
- else {
136
- // In production, use dynamic import.
137
- const url = (0, url_1.pathToFileURL)(targetPath).href;
138
- const module = await Promise.resolve(`${url}`).then(s => __importStar(require(s)));
139
- return module.default || module;
247
+ }
248
+ for (const dependentName of dependents) {
249
+ await this.unloadPlugin(dependentName, true);
250
+ }
251
+ await this._unloadSinglePlugin(pluginNameToUnload, ispending);
252
+ }
253
+ async _unloadSinglePlugin(pluginName, ispending = false) {
254
+ if (this.pluginStatus[pluginName] !== "enabled" /* PluginStatus.ENABLED */) {
255
+ return;
256
+ }
257
+ this.logger.info(`Unloading plugin "${pluginName}"...`);
258
+ try {
259
+ const plugin = this.plugins[pluginName];
260
+ if (plugin && plugin.disable)
261
+ await plugin.disable(this.getContext(pluginName));
262
+ this.unregall(pluginName);
263
+ delete this.plugins[pluginName];
264
+ delete this.pluginModules[pluginName];
265
+ this.pluginStatus[pluginName] = ispending ? "pending" /* PluginStatus.PENDING */ : "disabled" /* PluginStatus.DISABLED */;
266
+ if (this.pluginWatchers[pluginName]) {
267
+ this.pluginWatchers[pluginName].close();
268
+ delete this.pluginWatchers[pluginName];
140
269
  }
270
+ this.core.emit('plugin-unloaded', pluginName);
141
271
  }
142
- catch (e) {
143
- this.logger.error(`Error loading plugin from path ${pluginPath}:`, e);
144
- throw e;
272
+ catch (error) {
273
+ this.logger.error(`Failed to unload plugin "${pluginName}":`, error);
145
274
  }
146
275
  }
276
+ /**
277
+ * Reloads a single plugin's code by clearing the require cache and then reloading it.
278
+ * @param pluginName The name of the plugin to reload.
279
+ */
280
+ async reloadPlugin(pluginName) {
281
+ this.logger.info(`Reloading plugin code for "${pluginName}"...`);
282
+ this.clearRequireCache(pluginName, new Set());
283
+ await this.unloadPlugin(pluginName);
284
+ const success = await this.loadSinglePlugin(pluginName);
285
+ if (success) {
286
+ this.core.emit('plugin-reloaded', pluginName);
287
+ }
288
+ else {
289
+ this.logger.error(`Failed to reload plugin "${pluginName}". It may have unmet dependencies.`);
290
+ }
291
+ }
292
+ watchPlugin(pluginName, pluginPath) {
293
+ if (this.pluginWatchers[pluginName]) {
294
+ return;
295
+ }
296
+ const logger = new core_1.Logger('hmr');
297
+ const watcher = chokidar.watch(pluginPath, {
298
+ ignored: /(^|[\/])\../,
299
+ persistent: true,
300
+ ignoreInitial: true,
301
+ awaitWriteFinish: {
302
+ stabilityThreshold: 200,
303
+ pollInterval: 100,
304
+ },
305
+ });
306
+ watcher.on('change', async (changePath) => {
307
+ logger.info(`Plugin file changed: ${changePath}`);
308
+ await this.reloadPlugin(pluginName);
309
+ });
310
+ watcher.on('add', async (changePath) => {
311
+ logger.info(`New file added to plugin ${pluginName}: ${changePath}`);
312
+ await this.reloadPlugin(pluginName);
313
+ });
314
+ watcher.on('unlink', async (changePath) => {
315
+ logger.info(`File removed from plugin ${pluginName}: ${changePath}`);
316
+ await this.reloadPlugin(pluginName);
317
+ });
318
+ this.pluginWatchers[pluginName] = watcher;
319
+ }
320
+ async loadModule(pluginName) {
321
+ const plugin = await require(pluginName);
322
+ return plugin.default || plugin;
323
+ }
147
324
  clearRequireCache(moduleId, visited) {
148
325
  if (visited.has(moduleId)) {
149
- return; // Cycle detected
326
+ return;
150
327
  }
151
328
  visited.add(moduleId);
152
329
  const module = require.cache[moduleId];
153
330
  if (!module)
154
331
  return;
155
- // Recursively clear children's cache
156
332
  if (module.children) {
157
333
  for (const child of module.children) {
158
334
  this.clearRequireCache(child.id, visited);
159
335
  }
160
336
  }
161
- // Delete the module from cache
162
337
  delete require.cache[moduleId];
163
338
  }
164
- async unloadPlugin(pluginName) {
165
- if (!this.pluginCache[pluginName]) {
166
- return; // Plugin not loaded
167
- }
168
- delete this.pluginCache[pluginName];
169
- // With dynamic import, there's no direct equivalent of require.cache to clear.
170
- // The timestamp query in dev mode handles fresh loading.
171
- // For production, modules are cached intentionally.
172
- this.core?.unregall(pluginName);
173
- // this.logger.info(`Plugin unloaded: ${pluginName}`);
174
- }
175
339
  async checkPluginDependencies(pluginPath) {
176
- // TODO: Implement dependency checking from package.json
177
340
  return true;
178
341
  }
179
342
  async installPluginDependencies(pluginName) {
@@ -192,4 +355,5 @@ class PluginLoader {
192
355
  }
193
356
  }
194
357
  }
358
+ exports.PluginLoader = PluginLoader;
195
359
  exports.default = PluginLoader;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yumerijs/loader",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Module loader for yumeri",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -25,12 +25,19 @@
25
25
  },
26
26
  "homepage": "https://github.com/yumerijs/yumeri#readme",
27
27
  "devDependencies": {
28
+ "@types/js-yaml": "^4.0.9",
28
29
  "@types/node": "^22.13.10",
29
30
  "esbuild": "^0.25.9",
30
31
  "esbuild-register": "^3.6.0",
32
+ "@yumerijs/core": "^2.0.1",
31
33
  "typescript": "^5.8.2"
32
34
  },
33
35
  "dependencies": {
34
- "@yumerijs/core": "^2.0.0"
36
+ "@types/js-yaml": "^4.0.9",
37
+ "chokidar": "^4.0.3",
38
+ "js-yaml": "^4.1.0"
39
+ },
40
+ "peerDependencies": {
41
+ "@yumerijs/core": "^2.0.2"
35
42
  }
36
43
  }