@yumerijs/loader 2.0.0 → 2.0.1

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