@yumerijs/loader 3.1.0 → 3.1.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 +9 -1
- package/dist/index.js +109 -38
- package/package.json +1 -1
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,12 +53,18 @@ 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;
|
|
58
60
|
reloadPlugin(pluginName: string): Promise<void>;
|
|
59
61
|
private watchPlugin;
|
|
62
|
+
private isMissingModuleError;
|
|
63
|
+
private isNpxInvocation;
|
|
64
|
+
private detectPackageManager;
|
|
65
|
+
private confirmPluginInstall;
|
|
66
|
+
private installMissingPlugin;
|
|
67
|
+
private importPluginModule;
|
|
60
68
|
loadModule(pluginName: string): Promise<Plugin>;
|
|
61
69
|
checkPluginDependencies(pluginPath: string): Promise<boolean>;
|
|
62
70
|
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 {
|
|
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
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
10
12
|
export class PluginLoader {
|
|
11
13
|
pluginsDir;
|
|
12
14
|
core;
|
|
@@ -151,26 +153,20 @@ export class PluginLoader {
|
|
|
151
153
|
this.logger.info('No enabled plugins found in configuration.');
|
|
152
154
|
return;
|
|
153
155
|
}
|
|
154
|
-
|
|
155
|
-
while (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const success = await this.loadSinglePlugin(pluginName, false);
|
|
162
|
-
if (success) {
|
|
163
|
-
loadedInLastPass = true;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
156
|
+
// 第一阶段:将 optional 与 depend 一样处理,尽量让可选服务先完成加载并注入。
|
|
157
|
+
while (await this._loadPendingPlugins(true)) {
|
|
158
|
+
// Continue scanning until no plugin can satisfy all required and optional dependencies.
|
|
159
|
+
}
|
|
160
|
+
// 第二阶段:严格扫描无法推进后,忽略尚未提供的 optional;depend 仍必须满足。
|
|
161
|
+
while (await this._loadPendingPlugins(false)) {
|
|
162
|
+
// Continue scanning in relaxed mode until only genuinely missing dependencies remain.
|
|
166
163
|
}
|
|
167
|
-
await this._loadPendingPlugins();
|
|
168
164
|
const pendingPlugins = Object.keys(this.pluginStatus).filter(p => this.pluginStatus[p] === "pending" /* PluginStatus.PENDING */);
|
|
169
165
|
if (pendingPlugins.length > 0) {
|
|
170
|
-
|
|
166
|
+
this.logger.warn('Some plugins could not be loaded due to unresolved required dependencies:', pendingPlugins);
|
|
171
167
|
}
|
|
172
168
|
}
|
|
173
|
-
async loadSinglePlugin(pluginName, triggerPendingCheck = true, onlypending = false) {
|
|
169
|
+
async loadSinglePlugin(pluginName, triggerPendingCheck = true, onlypending = false, requireOptionalDependencies = true) {
|
|
174
170
|
if (!this.pluginStatus[pluginName]) {
|
|
175
171
|
this.pluginStatus[pluginName] = "pending" /* PluginStatus.PENDING */;
|
|
176
172
|
}
|
|
@@ -209,7 +205,10 @@ export class PluginLoader {
|
|
|
209
205
|
}
|
|
210
206
|
}
|
|
211
207
|
}
|
|
212
|
-
const deps =
|
|
208
|
+
const deps = [
|
|
209
|
+
...(pluginInstance.depend || []),
|
|
210
|
+
...(requireOptionalDependencies ? (pluginInstance.optional || []) : []),
|
|
211
|
+
];
|
|
213
212
|
const unmetDependencies = deps.filter(dep => !this.core.components[dep] && !this.core.services[dep]);
|
|
214
213
|
if (unmetDependencies.length > 0) {
|
|
215
214
|
return false;
|
|
@@ -229,7 +228,7 @@ export class PluginLoader {
|
|
|
229
228
|
await this.core.plugin(pluginInstance, context, finalConfig);
|
|
230
229
|
this.pluginStatus[pluginName] = "enabled" /* PluginStatus.ENABLED */;
|
|
231
230
|
if (triggerPendingCheck) {
|
|
232
|
-
await this._loadPendingPlugins();
|
|
231
|
+
await this._loadPendingPlugins(requireOptionalDependencies);
|
|
233
232
|
}
|
|
234
233
|
if (this.isDev) {
|
|
235
234
|
let pluginPathToWatch = null;
|
|
@@ -263,13 +262,14 @@ export class PluginLoader {
|
|
|
263
262
|
return false;
|
|
264
263
|
}
|
|
265
264
|
}
|
|
266
|
-
async _loadPendingPlugins() {
|
|
265
|
+
async _loadPendingPlugins(requireOptionalDependencies = true) {
|
|
267
266
|
const pendingPlugins = Object.keys(this.pluginStatus).filter(p => this.pluginStatus[p] === "pending" /* PluginStatus.PENDING */);
|
|
268
|
-
|
|
269
|
-
return;
|
|
267
|
+
let loadedAny = false;
|
|
270
268
|
for (const pluginName of pendingPlugins) {
|
|
271
|
-
await this.loadSinglePlugin(pluginName, false);
|
|
269
|
+
const loaded = await this.loadSinglePlugin(pluginName, false, true, requireOptionalDependencies);
|
|
270
|
+
loadedAny ||= loaded;
|
|
272
271
|
}
|
|
272
|
+
return loadedAny;
|
|
273
273
|
}
|
|
274
274
|
async unloadPlugin(pluginNameToUnload, ispending = false) {
|
|
275
275
|
const dependents = [];
|
|
@@ -323,7 +323,10 @@ export class PluginLoader {
|
|
|
323
323
|
this.logger.info(`Reloading plugin: "${pluginName}"...`);
|
|
324
324
|
await this.reloadConfigFile();
|
|
325
325
|
await this.unloadPlugin(pluginName, true);
|
|
326
|
-
|
|
326
|
+
let success = await this.loadSinglePlugin(pluginName, true, false, true);
|
|
327
|
+
if (!success) {
|
|
328
|
+
success = await this.loadSinglePlugin(pluginName, true, false, false);
|
|
329
|
+
}
|
|
327
330
|
if (success) {
|
|
328
331
|
this.logger.info(`Plugin "${pluginName}" reloaded successfully.`);
|
|
329
332
|
this.core.emit('plugin-reloaded', pluginName);
|
|
@@ -360,11 +363,91 @@ export class PluginLoader {
|
|
|
360
363
|
});
|
|
361
364
|
this.pluginWatchers[pluginName] = watcher;
|
|
362
365
|
}
|
|
366
|
+
isMissingModuleError(error, moduleName) {
|
|
367
|
+
const candidate = error;
|
|
368
|
+
if (candidate?.code !== 'ERR_MODULE_NOT_FOUND')
|
|
369
|
+
return false;
|
|
370
|
+
return typeof candidate.message === 'string' && candidate.message.includes(moduleName);
|
|
371
|
+
}
|
|
372
|
+
isNpxInvocation() {
|
|
373
|
+
const argv = process.argv.map(value => value.toLowerCase());
|
|
374
|
+
const env = process.env;
|
|
375
|
+
return Boolean(env.npm_config_npx_command ||
|
|
376
|
+
env.npm_command === 'exec' ||
|
|
377
|
+
argv.some(value => /(?:^|[\\/])npx(?:\.cmd)?$/.test(value)) ||
|
|
378
|
+
(env._ && /(?:^|[\\/])npx(?:\.cmd)?$/.test(env._.toLowerCase())));
|
|
379
|
+
}
|
|
380
|
+
detectPackageManager() {
|
|
381
|
+
const cwd = process.cwd();
|
|
382
|
+
try {
|
|
383
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
384
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
385
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
386
|
+
if (typeof packageJson.packageManager === 'string') {
|
|
387
|
+
return packageJson.packageManager.split('@')[0];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// Fall back to lockfile detection when package.json is unavailable or invalid.
|
|
393
|
+
}
|
|
394
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml')))
|
|
395
|
+
return 'pnpm';
|
|
396
|
+
if (fs.existsSync(path.join(cwd, 'yarn.lock')))
|
|
397
|
+
return 'yarn';
|
|
398
|
+
if (fs.existsSync(path.join(cwd, 'bun.lockb')) || fs.existsSync(path.join(cwd, 'bun.lock')))
|
|
399
|
+
return 'bun';
|
|
400
|
+
return 'npm';
|
|
401
|
+
}
|
|
402
|
+
async confirmPluginInstall(moduleName) {
|
|
403
|
+
if (!input.isTTY || !output.isTTY) {
|
|
404
|
+
this.logger.warn(`Plugin package "${moduleName}" is not installed and no interactive terminal is available.`);
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
const scope = this.isNpxInvocation() ? 'globally' : 'in the current project';
|
|
408
|
+
const readline = createInterface({ input, output });
|
|
409
|
+
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());
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
readline.close();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async installMissingPlugin(moduleName) {
|
|
418
|
+
const packageManager = this.detectPackageManager();
|
|
419
|
+
const global = this.isNpxInvocation();
|
|
420
|
+
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],
|
|
425
|
+
};
|
|
426
|
+
const args = argsByManager[packageManager] || argsByManager.npm;
|
|
427
|
+
const command = process.platform === 'win32' ? `${packageManager}.cmd` : packageManager;
|
|
428
|
+
this.logger.info(`Installing missing plugin "${moduleName}" with ${command} ${args.join(' ')}` +
|
|
429
|
+
(global ? ' (global)' : ''));
|
|
430
|
+
await execFileAsync(command, args, { cwd: process.cwd() });
|
|
431
|
+
}
|
|
432
|
+
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
|
+
}
|
|
445
|
+
}
|
|
363
446
|
async loadModule(pluginName) {
|
|
364
447
|
const entry = this.getPluginEntry(pluginName);
|
|
365
448
|
if (!entry)
|
|
366
449
|
throw new Error(`Plugin configuration not found for instance "${pluginName}".`);
|
|
367
|
-
const pluginModule = await
|
|
450
|
+
const pluginModule = await this.importPluginModule(entry.moduleName);
|
|
368
451
|
const context = this.getContext(pluginName, {});
|
|
369
452
|
return resolvePluginModule(pluginModule, context, entry.config);
|
|
370
453
|
}
|
|
@@ -372,19 +455,7 @@ export class PluginLoader {
|
|
|
372
455
|
return true;
|
|
373
456
|
}
|
|
374
457
|
async installPluginDependencies(pluginName) {
|
|
375
|
-
|
|
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
|
-
}
|
|
458
|
+
await this.installMissingPlugin(pluginName);
|
|
388
459
|
}
|
|
389
460
|
}
|
|
390
461
|
export default PluginLoader;
|