@yumerijs/loader 3.1.1 → 3.1.3

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
@@ -3,6 +3,7 @@ interface Plugin {
3
3
  apply: (ctx: Context, config: Config) => Promise<void>;
4
4
  disable: (ctx: Context) => Promise<void>;
5
5
  depend: Array<string>;
6
+ optional?: Array<string>;
6
7
  provide: Array<string>;
7
8
  render?: string;
8
9
  config?: Schema<any>;
@@ -23,6 +24,7 @@ export declare class PluginLoader {
23
24
  plugins: {
24
25
  [name: string]: Plugin & {
25
26
  depend?: string[];
27
+ optional?: string[];
26
28
  provide?: string[];
27
29
  };
28
30
  };
@@ -51,7 +53,7 @@ export declare class PluginLoader {
51
53
  unregall(pluginName: string): void;
52
54
  loadConfig(configPath: string): Promise<void>;
53
55
  loadPlugins(): Promise<void>;
54
- loadSinglePlugin(pluginName: string, triggerPendingCheck?: boolean, onlypending?: boolean): Promise<boolean>;
56
+ loadSinglePlugin(pluginName: string, triggerPendingCheck?: boolean, onlypending?: boolean, requireOptionalDependencies?: boolean): Promise<boolean>;
55
57
  private _loadPendingPlugins;
56
58
  unloadPlugin(pluginNameToUnload: string, ispending?: boolean): Promise<void>;
57
59
  private _unloadSinglePlugin;
@@ -61,6 +63,8 @@ export declare class PluginLoader {
61
63
  private isNpxInvocation;
62
64
  private detectPackageManager;
63
65
  private confirmPluginInstall;
66
+ private isPluginModuleResolvable;
67
+ private installMissingPluginModules;
64
68
  private installMissingPlugin;
65
69
  private importPluginModule;
66
70
  loadModule(pluginName: string): Promise<Plugin>;
package/dist/index.js CHANGED
@@ -153,26 +153,22 @@ export class PluginLoader {
153
153
  this.logger.info('No enabled plugins found in configuration.');
154
154
  return;
155
155
  }
156
- let loadedInLastPass = true;
157
- while (loadedInLastPass) {
158
- loadedInLastPass = false;
159
- for (const pluginName of enabledPlugins) {
160
- if (this.pluginStatus[pluginName] === "enabled" /* PluginStatus.ENABLED */) {
161
- continue;
162
- }
163
- const success = await this.loadSinglePlugin(pluginName, false);
164
- if (success) {
165
- loadedInLastPass = true;
166
- }
167
- }
156
+ // Resolve and install every configured plugin before any plugin module is loaded.
157
+ await this.installMissingPluginModules(allEntries.filter(entry => entry.enabled));
158
+ // 第一阶段:将 optional 与 depend 一样处理,尽量让可选服务先完成加载并注入。
159
+ while (await this._loadPendingPlugins(true)) {
160
+ // Continue scanning until no plugin can satisfy all required and optional dependencies.
161
+ }
162
+ // 第二阶段:严格扫描无法推进后,忽略尚未提供的 optional;depend 仍必须满足。
163
+ while (await this._loadPendingPlugins(false)) {
164
+ // Continue scanning in relaxed mode until only genuinely missing dependencies remain.
168
165
  }
169
- await this._loadPendingPlugins();
170
166
  const pendingPlugins = Object.keys(this.pluginStatus).filter(p => this.pluginStatus[p] === "pending" /* PluginStatus.PENDING */);
171
167
  if (pendingPlugins.length > 0) {
172
- // this.logger.warn('Some plugins could not be loaded due to unresolved dependencies:', pendingPlugins);
168
+ this.logger.warn('Some plugins could not be loaded due to unresolved required dependencies:', pendingPlugins);
173
169
  }
174
170
  }
175
- async loadSinglePlugin(pluginName, triggerPendingCheck = true, onlypending = false) {
171
+ async loadSinglePlugin(pluginName, triggerPendingCheck = true, onlypending = false, requireOptionalDependencies = true) {
176
172
  if (!this.pluginStatus[pluginName]) {
177
173
  this.pluginStatus[pluginName] = "pending" /* PluginStatus.PENDING */;
178
174
  }
@@ -211,7 +207,10 @@ export class PluginLoader {
211
207
  }
212
208
  }
213
209
  }
214
- const deps = pluginInstance.depend || [];
210
+ const deps = [
211
+ ...(pluginInstance.depend || []),
212
+ ...(requireOptionalDependencies ? (pluginInstance.optional || []) : []),
213
+ ];
215
214
  const unmetDependencies = deps.filter(dep => !this.core.components[dep] && !this.core.services[dep]);
216
215
  if (unmetDependencies.length > 0) {
217
216
  return false;
@@ -231,7 +230,7 @@ export class PluginLoader {
231
230
  await this.core.plugin(pluginInstance, context, finalConfig);
232
231
  this.pluginStatus[pluginName] = "enabled" /* PluginStatus.ENABLED */;
233
232
  if (triggerPendingCheck) {
234
- await this._loadPendingPlugins();
233
+ await this._loadPendingPlugins(requireOptionalDependencies);
235
234
  }
236
235
  if (this.isDev) {
237
236
  let pluginPathToWatch = null;
@@ -265,13 +264,14 @@ export class PluginLoader {
265
264
  return false;
266
265
  }
267
266
  }
268
- async _loadPendingPlugins() {
267
+ async _loadPendingPlugins(requireOptionalDependencies = true) {
269
268
  const pendingPlugins = Object.keys(this.pluginStatus).filter(p => this.pluginStatus[p] === "pending" /* PluginStatus.PENDING */);
270
- if (pendingPlugins.length === 0)
271
- return;
269
+ let loadedAny = false;
272
270
  for (const pluginName of pendingPlugins) {
273
- await this.loadSinglePlugin(pluginName, false);
271
+ const loaded = await this.loadSinglePlugin(pluginName, false, true, requireOptionalDependencies);
272
+ loadedAny ||= loaded;
274
273
  }
274
+ return loadedAny;
275
275
  }
276
276
  async unloadPlugin(pluginNameToUnload, ispending = false) {
277
277
  const dependents = [];
@@ -325,7 +325,10 @@ export class PluginLoader {
325
325
  this.logger.info(`Reloading plugin: "${pluginName}"...`);
326
326
  await this.reloadConfigFile();
327
327
  await this.unloadPlugin(pluginName, true);
328
- const success = await this.loadSinglePlugin(pluginName, true, false);
328
+ let success = await this.loadSinglePlugin(pluginName, true, false, true);
329
+ if (!success) {
330
+ success = await this.loadSinglePlugin(pluginName, true, false, false);
331
+ }
329
332
  if (success) {
330
333
  this.logger.info(`Plugin "${pluginName}" reloaded successfully.`);
331
334
  this.core.emit('plugin-reloaded', pluginName);
@@ -413,7 +416,36 @@ export class PluginLoader {
413
416
  readline.close();
414
417
  }
415
418
  }
419
+ isPluginModuleResolvable(moduleName) {
420
+ try {
421
+ import.meta.resolve(moduleName);
422
+ return true;
423
+ }
424
+ catch (error) {
425
+ if (this.isMissingModuleError(error, moduleName))
426
+ return false;
427
+ throw error;
428
+ }
429
+ }
430
+ async installMissingPluginModules(entries) {
431
+ const missingModules = [...new Set(entries.map(entry => entry.moduleName))]
432
+ .filter(moduleName => !this.isPluginModuleResolvable(moduleName));
433
+ for (const moduleName of missingModules) {
434
+ const shouldInstall = await this.confirmPluginInstall(moduleName);
435
+ if (!shouldInstall)
436
+ continue;
437
+ try {
438
+ await this.installMissingPlugin(moduleName);
439
+ }
440
+ catch (error) {
441
+ this.logger.error(`Failed to install missing plugin "${moduleName}":`, error);
442
+ }
443
+ }
444
+ }
416
445
  async installMissingPlugin(moduleName) {
446
+ if (!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(moduleName)) {
447
+ throw new Error(`Cannot automatically install invalid package name "${moduleName}".`);
448
+ }
417
449
  const packageManager = this.detectPackageManager();
418
450
  const global = this.isNpxInvocation();
419
451
  const argsByManager = {
@@ -426,21 +458,21 @@ export class PluginLoader {
426
458
  const command = process.platform === 'win32' ? `${packageManager}.cmd` : packageManager;
427
459
  this.logger.info(`Installing missing plugin "${moduleName}" with ${command} ${args.join(' ')}` +
428
460
  (global ? ' (global)' : ''));
461
+ // Windows command shims (for example npm.cmd) require cmd.exe; execFile
462
+ // otherwise fails before the package manager can start with spawn EINVAL.
463
+ if (process.platform === 'win32') {
464
+ await execFileAsync(process.env.ComSpec || 'cmd.exe', [
465
+ '/d',
466
+ '/s',
467
+ '/c',
468
+ `${command} ${args.join(' ')}`,
469
+ ], { cwd: process.cwd() });
470
+ return;
471
+ }
429
472
  await execFileAsync(command, args, { cwd: process.cwd() });
430
473
  }
431
474
  async importPluginModule(moduleName) {
432
- try {
433
- return await import(moduleName);
434
- }
435
- catch (error) {
436
- if (!this.isMissingModuleError(error, moduleName))
437
- throw error;
438
- const shouldInstall = await this.confirmPluginInstall(moduleName);
439
- if (!shouldInstall)
440
- throw error;
441
- await this.installMissingPlugin(moduleName);
442
- return await import(moduleName);
443
- }
475
+ return await import(moduleName);
444
476
  }
445
477
  async loadModule(pluginName) {
446
478
  const entry = this.getPluginEntry(pluginName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yumerijs/loader",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
4
4
  "description": "Module loader for yumeri",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",