@yumerijs/loader 3.1.2 → 3.1.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.
package/dist/index.d.ts CHANGED
@@ -34,6 +34,8 @@ export declare class PluginLoader {
34
34
  private configPath;
35
35
  private pluginContexts;
36
36
  private isDev;
37
+ /** Install every missing configured plugin without prompting. */
38
+ autoInstallMissingPlugins: boolean;
37
39
  constructor(core?: Core, pluginsDir?: string);
38
40
  /**
39
41
  * Normalize both the legacy `{ "package-name": { ...config } }` form and
@@ -52,7 +54,7 @@ export declare class PluginLoader {
52
54
  getContext(pluginName: string, injections?: Record<string, any>): Context;
53
55
  unregall(pluginName: string): void;
54
56
  loadConfig(configPath: string): Promise<void>;
55
- loadPlugins(): Promise<void>;
57
+ loadPlugins(): Promise<boolean>;
56
58
  loadSinglePlugin(pluginName: string, triggerPendingCheck?: boolean, onlypending?: boolean, requireOptionalDependencies?: boolean): Promise<boolean>;
57
59
  private _loadPendingPlugins;
58
60
  unloadPlugin(pluginNameToUnload: string, ispending?: boolean): Promise<void>;
@@ -63,6 +65,8 @@ export declare class PluginLoader {
63
65
  private isNpxInvocation;
64
66
  private detectPackageManager;
65
67
  private confirmPluginInstall;
68
+ private isPluginModuleResolvable;
69
+ private installMissingPluginModules;
66
70
  private installMissingPlugin;
67
71
  private importPluginModule;
68
72
  loadModule(pluginName: string): Promise<Plugin>;
package/dist/index.js CHANGED
@@ -3,8 +3,6 @@ import { Core, Logger, Context, fallback, Schema, I18n, resolvePluginModule } fr
3
3
  import * as fs from 'fs';
4
4
  import { promisify } from 'util';
5
5
  import { execFile } from 'child_process';
6
- import { createInterface } from 'readline/promises';
7
- import { stdin as input, stdout as output } from 'process';
8
6
  import { fileURLToPath } from 'url';
9
7
  import * as yaml from 'js-yaml';
10
8
  import * as chokidar from 'chokidar';
@@ -21,6 +19,8 @@ export class PluginLoader {
21
19
  configPath = '';
22
20
  pluginContexts = {};
23
21
  isDev = false;
22
+ /** Install every missing configured plugin without prompting. */
23
+ autoInstallMissingPlugins = false;
24
24
  constructor(core, pluginsDir = 'plugins') {
25
25
  this.pluginsDir = pluginsDir;
26
26
  this.core = core || new Core(this, undefined, false);
@@ -133,7 +133,7 @@ export class PluginLoader {
133
133
  async loadPlugins() {
134
134
  if (!this.config || typeof this.config.plugins !== 'object' || this.config.plugins === null) {
135
135
  this.logger.info('No plugins configuration found. No plugins to load.');
136
- return;
136
+ return false;
137
137
  }
138
138
  const allEntries = Object.keys(this.config.plugins)
139
139
  .map(key => this.getPluginEntry(key.startsWith('~') ? key.substring(1) : key))
@@ -151,7 +151,13 @@ export class PluginLoader {
151
151
  }
152
152
  if (enabledPlugins.length === 0) {
153
153
  this.logger.info('No enabled plugins found in configuration.');
154
- return;
154
+ return false;
155
+ }
156
+ // Resolve and install every configured plugin before any plugin module is loaded.
157
+ const installedMissingPlugins = await this.installMissingPluginModules(allEntries.filter(entry => entry.enabled));
158
+ if (installedMissingPlugins) {
159
+ this.logger.info('Missing plugin packages were installed. A worker restart is required before loading plugins.');
160
+ return true;
155
161
  }
156
162
  // 第一阶段:将 optional 与 depend 一样处理,尽量让可选服务先完成加载并注入。
157
163
  while (await this._loadPendingPlugins(true)) {
@@ -165,6 +171,7 @@ export class PluginLoader {
165
171
  if (pendingPlugins.length > 0) {
166
172
  this.logger.warn('Some plugins could not be loaded due to unresolved required dependencies:', pendingPlugins);
167
173
  }
174
+ return false;
168
175
  }
169
176
  async loadSinglePlugin(pluginName, triggerPendingCheck = true, onlypending = false, requireOptionalDependencies = true) {
170
177
  if (!this.pluginStatus[pluginName]) {
@@ -400,48 +407,91 @@ export class PluginLoader {
400
407
  return 'npm';
401
408
  }
402
409
  async confirmPluginInstall(moduleName) {
403
- if (!input.isTTY || !output.isTTY) {
410
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
404
411
  this.logger.warn(`Plugin package "${moduleName}" is not installed and no interactive terminal is available.`);
405
412
  return false;
406
413
  }
407
414
  const scope = this.isNpxInvocation() ? 'globally' : 'in the current project';
408
- const readline = createInterface({ input, output });
415
+ const answer = await this.logger.input(`Plugin package "${moduleName}" is not installed. Install it ${scope}? [y/N] `);
416
+ return /^(y|yes)$/i.test(answer.trim());
417
+ }
418
+ isPluginModuleResolvable(moduleName) {
409
419
  try {
410
- const answer = await readline.question(`Plugin package "${moduleName}" is not installed. Install it ${scope}? [y/N] `);
411
- return /^(y|yes)$/i.test(answer.trim());
420
+ import.meta.resolve(moduleName);
421
+ return true;
422
+ }
423
+ catch (error) {
424
+ if (this.isMissingModuleError(error, moduleName))
425
+ return false;
426
+ throw error;
427
+ }
428
+ }
429
+ async installMissingPluginModules(entries) {
430
+ const missingModules = [...new Set(entries.map(entry => entry.moduleName))]
431
+ .filter(moduleName => !this.isPluginModuleResolvable(moduleName));
432
+ if (missingModules.length === 0)
433
+ return false;
434
+ if (this.autoInstallMissingPlugins) {
435
+ this.logger.info(`Automatically installing missing plugin packages: ${missingModules.join(', ')}`);
436
+ try {
437
+ await this.installMissingPlugin(missingModules);
438
+ return true;
439
+ }
440
+ catch (error) {
441
+ this.logger.error('Failed to automatically install missing plugin packages:', error);
442
+ return false;
443
+ }
412
444
  }
413
- finally {
414
- readline.close();
445
+ let installedAny = false;
446
+ for (const moduleName of missingModules) {
447
+ const shouldInstall = await this.confirmPluginInstall(moduleName);
448
+ if (!shouldInstall)
449
+ continue;
450
+ try {
451
+ await this.installMissingPlugin(moduleName);
452
+ installedAny = true;
453
+ }
454
+ catch (error) {
455
+ this.logger.error(`Failed to install missing plugin "${moduleName}":`, error);
456
+ }
415
457
  }
458
+ return installedAny;
416
459
  }
417
- async installMissingPlugin(moduleName) {
460
+ async installMissingPlugin(moduleNames) {
461
+ const packages = Array.isArray(moduleNames) ? moduleNames : [moduleNames];
462
+ for (const moduleName of packages) {
463
+ if (!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(moduleName)) {
464
+ throw new Error(`Cannot automatically install invalid package name "${moduleName}".`);
465
+ }
466
+ }
418
467
  const packageManager = this.detectPackageManager();
419
468
  const global = this.isNpxInvocation();
420
469
  const argsByManager = {
421
- npm: global ? ['install', '--global', moduleName] : ['install', moduleName, '--save'],
422
- yarn: global ? ['global', 'add', moduleName] : ['add', moduleName],
423
- pnpm: global ? ['add', '--global', moduleName] : ['add', moduleName],
424
- bun: global ? ['add', '--global', moduleName] : ['add', moduleName],
470
+ npm: global ? ['install', '--global', ...packages] : ['install', ...packages, '--save'],
471
+ yarn: global ? ['global', 'add', ...packages] : ['add', ...packages],
472
+ pnpm: global ? ['add', '--global', ...packages] : ['add', ...packages],
473
+ bun: global ? ['add', '--global', ...packages] : ['add', ...packages],
425
474
  };
426
475
  const args = argsByManager[packageManager] || argsByManager.npm;
427
476
  const command = process.platform === 'win32' ? `${packageManager}.cmd` : packageManager;
428
- this.logger.info(`Installing missing plugin "${moduleName}" with ${command} ${args.join(' ')}` +
477
+ const packageLabel = packages.map(moduleName => `"${moduleName}"`).join(', ');
478
+ this.logger.info(`Installing missing plugin package${packages.length === 1 ? '' : 's'} ${packageLabel} with ${command} ${args.join(' ')}` +
429
479
  (global ? ' (global)' : ''));
480
+ // Windows command shims (for example npm.cmd) require cmd.exe; execFile
481
+ // otherwise fails before the package manager can start with spawn EINVAL.
482
+ if (process.platform === 'win32') {
483
+ await execFileAsync(process.env.ComSpec || 'cmd.exe', [
484
+ '/d',
485
+ '/s',
486
+ '/c',
487
+ `${command} ${args.join(' ')}`,
488
+ ], { cwd: process.cwd() });
489
+ return;
490
+ }
430
491
  await execFileAsync(command, args, { cwd: process.cwd() });
431
492
  }
432
493
  async importPluginModule(moduleName) {
433
- try {
434
- return await import(moduleName);
435
- }
436
- catch (error) {
437
- if (!this.isMissingModuleError(error, moduleName))
438
- throw error;
439
- const shouldInstall = await this.confirmPluginInstall(moduleName);
440
- if (!shouldInstall)
441
- throw error;
442
- await this.installMissingPlugin(moduleName);
443
- return await import(moduleName);
444
- }
494
+ return await import(moduleName);
445
495
  }
446
496
  async loadModule(pluginName) {
447
497
  const entry = this.getPluginEntry(pluginName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yumerijs/loader",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
4
4
  "description": "Module loader for yumeri",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -29,7 +29,7 @@
29
29
  "devDependencies": {
30
30
  "@types/js-yaml": "^4.0.9",
31
31
  "@types/node": "^22.13.10",
32
- "@yumerijs/core": "^3.0.5",
32
+ "@yumerijs/core": "^3.0.7",
33
33
  "esbuild-register": "^3.6.0",
34
34
  "typescript": "^6.0.3"
35
35
  },