@yumerijs/loader 3.1.0 → 3.1.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
@@ -57,6 +57,12 @@ export declare class PluginLoader {
57
57
  private _unloadSinglePlugin;
58
58
  reloadPlugin(pluginName: string): Promise<void>;
59
59
  private watchPlugin;
60
+ private isMissingModuleError;
61
+ private isNpxInvocation;
62
+ private detectPackageManager;
63
+ private confirmPluginInstall;
64
+ private installMissingPlugin;
65
+ private importPluginModule;
60
66
  loadModule(pluginName: string): Promise<Plugin>;
61
67
  checkPluginDependencies(pluginPath: string): Promise<boolean>;
62
68
  installPluginDependencies(pluginName: string): Promise<void>;
package/dist/index.js CHANGED
@@ -2,11 +2,13 @@ import * as path from 'path';
2
2
  import { Core, Logger, Context, fallback, Schema, I18n, resolvePluginModule } from '@yumerijs/core';
3
3
  import * as fs from 'fs';
4
4
  import { promisify } from 'util';
5
- import { exec } from 'child_process';
5
+ import { execFile } from 'child_process';
6
+ import { createInterface } from 'readline/promises';
7
+ import { stdin as input, stdout as output } from 'process';
6
8
  import { fileURLToPath } from 'url';
7
9
  import * as yaml from 'js-yaml';
8
10
  import * as chokidar from 'chokidar';
9
- const execAsync = promisify(exec);
11
+ const execFileAsync = promisify(execFile);
10
12
  export class PluginLoader {
11
13
  pluginsDir;
12
14
  core;
@@ -360,11 +362,91 @@ export class PluginLoader {
360
362
  });
361
363
  this.pluginWatchers[pluginName] = watcher;
362
364
  }
365
+ isMissingModuleError(error, moduleName) {
366
+ const candidate = error;
367
+ if (candidate?.code !== 'ERR_MODULE_NOT_FOUND')
368
+ return false;
369
+ return typeof candidate.message === 'string' && candidate.message.includes(moduleName);
370
+ }
371
+ isNpxInvocation() {
372
+ const argv = process.argv.map(value => value.toLowerCase());
373
+ const env = process.env;
374
+ return Boolean(env.npm_config_npx_command ||
375
+ env.npm_command === 'exec' ||
376
+ argv.some(value => /(?:^|[\\/])npx(?:\.cmd)?$/.test(value)) ||
377
+ (env._ && /(?:^|[\\/])npx(?:\.cmd)?$/.test(env._.toLowerCase())));
378
+ }
379
+ detectPackageManager() {
380
+ const cwd = process.cwd();
381
+ try {
382
+ const packageJsonPath = path.join(cwd, 'package.json');
383
+ if (fs.existsSync(packageJsonPath)) {
384
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
385
+ if (typeof packageJson.packageManager === 'string') {
386
+ return packageJson.packageManager.split('@')[0];
387
+ }
388
+ }
389
+ }
390
+ catch {
391
+ // Fall back to lockfile detection when package.json is unavailable or invalid.
392
+ }
393
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml')))
394
+ return 'pnpm';
395
+ if (fs.existsSync(path.join(cwd, 'yarn.lock')))
396
+ return 'yarn';
397
+ if (fs.existsSync(path.join(cwd, 'bun.lockb')) || fs.existsSync(path.join(cwd, 'bun.lock')))
398
+ return 'bun';
399
+ return 'npm';
400
+ }
401
+ async confirmPluginInstall(moduleName) {
402
+ if (!input.isTTY || !output.isTTY) {
403
+ this.logger.warn(`Plugin package "${moduleName}" is not installed and no interactive terminal is available.`);
404
+ return false;
405
+ }
406
+ const scope = this.isNpxInvocation() ? 'globally' : 'in the current project';
407
+ const readline = createInterface({ input, output });
408
+ try {
409
+ const answer = await readline.question(`Plugin package "${moduleName}" is not installed. Install it ${scope}? [y/N] `);
410
+ return /^(y|yes)$/i.test(answer.trim());
411
+ }
412
+ finally {
413
+ readline.close();
414
+ }
415
+ }
416
+ async installMissingPlugin(moduleName) {
417
+ const packageManager = this.detectPackageManager();
418
+ const global = this.isNpxInvocation();
419
+ const argsByManager = {
420
+ npm: global ? ['install', '--global', moduleName] : ['install', moduleName, '--save'],
421
+ yarn: global ? ['global', 'add', moduleName] : ['add', moduleName],
422
+ pnpm: global ? ['add', '--global', moduleName] : ['add', moduleName],
423
+ bun: global ? ['add', '--global', moduleName] : ['add', moduleName],
424
+ };
425
+ const args = argsByManager[packageManager] || argsByManager.npm;
426
+ const command = process.platform === 'win32' ? `${packageManager}.cmd` : packageManager;
427
+ this.logger.info(`Installing missing plugin "${moduleName}" with ${command} ${args.join(' ')}` +
428
+ (global ? ' (global)' : ''));
429
+ await execFileAsync(command, args, { cwd: process.cwd() });
430
+ }
431
+ 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
+ }
444
+ }
363
445
  async loadModule(pluginName) {
364
446
  const entry = this.getPluginEntry(pluginName);
365
447
  if (!entry)
366
448
  throw new Error(`Plugin configuration not found for instance "${pluginName}".`);
367
- const pluginModule = await import(entry.moduleName);
449
+ const pluginModule = await this.importPluginModule(entry.moduleName);
368
450
  const context = this.getContext(pluginName, {});
369
451
  return resolvePluginModule(pluginModule, context, entry.config);
370
452
  }
@@ -372,19 +454,7 @@ export class PluginLoader {
372
454
  return true;
373
455
  }
374
456
  async installPluginDependencies(pluginName) {
375
- try {
376
- this.logger.info(`Installing dependencies for plugin: ${pluginName}`);
377
- const { stdout, stderr } = await execAsync(`npm install ${pluginName} --save`);
378
- this.logger.info(`stdout: ${stdout}`);
379
- if (stderr) {
380
- this.logger.error(`stderr: ${stderr}`);
381
- }
382
- this.logger.info(`Dependencies installed for plugin: ${pluginName}`);
383
- }
384
- catch (error) {
385
- this.logger.error(`Error installing dependencies for plugin ${pluginName}:`, error);
386
- throw error;
387
- }
457
+ await this.installMissingPlugin(pluginName);
388
458
  }
389
459
  }
390
460
  export default PluginLoader;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yumerijs/loader",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "Module loader for yumeri",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",