@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 +5 -1
- package/dist/index.js +66 -34
- 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,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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|