@yumerijs/loader 3.0.1 → 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 +21 -0
- package/dist/index.js +156 -38
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,14 @@ interface Plugin {
|
|
|
7
7
|
render?: string;
|
|
8
8
|
config?: Schema<any>;
|
|
9
9
|
}
|
|
10
|
+
export interface PluginConfigEntry {
|
|
11
|
+
id: string;
|
|
12
|
+
moduleName: string;
|
|
13
|
+
config: any;
|
|
14
|
+
configKey: string;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
legacy: boolean;
|
|
17
|
+
}
|
|
10
18
|
export declare class PluginLoader {
|
|
11
19
|
private pluginsDir;
|
|
12
20
|
core: Core;
|
|
@@ -25,6 +33,13 @@ export declare class PluginLoader {
|
|
|
25
33
|
private pluginContexts;
|
|
26
34
|
private isDev;
|
|
27
35
|
constructor(core?: Core, pluginsDir?: string);
|
|
36
|
+
/**
|
|
37
|
+
* Normalize both the legacy `{ "package-name": { ...config } }` form and
|
|
38
|
+
* the named instance form `{ "instance-id": { module, config } }`.
|
|
39
|
+
*/
|
|
40
|
+
getPluginEntry(pluginId: string): PluginConfigEntry | undefined;
|
|
41
|
+
setPluginConfig(pluginId: string, config: any): boolean;
|
|
42
|
+
private getEnabledPluginEntries;
|
|
28
43
|
/**
|
|
29
44
|
* Reloads the config file from disk into memory and emits a 'config-reloaded' event.
|
|
30
45
|
* This does NOT reload any plugins.
|
|
@@ -42,6 +57,12 @@ export declare class PluginLoader {
|
|
|
42
57
|
private _unloadSinglePlugin;
|
|
43
58
|
reloadPlugin(pluginName: string): Promise<void>;
|
|
44
59
|
private watchPlugin;
|
|
60
|
+
private isMissingModuleError;
|
|
61
|
+
private isNpxInvocation;
|
|
62
|
+
private detectPackageManager;
|
|
63
|
+
private confirmPluginInstall;
|
|
64
|
+
private installMissingPlugin;
|
|
65
|
+
private importPluginModule;
|
|
45
66
|
loadModule(pluginName: string): Promise<Plugin>;
|
|
46
67
|
checkPluginDependencies(pluginPath: string): Promise<boolean>;
|
|
47
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 {
|
|
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;
|
|
@@ -25,6 +27,55 @@ export class PluginLoader {
|
|
|
25
27
|
this.isDev = process.env.NODE_ENV === 'development';
|
|
26
28
|
Logger.setCore(this.core);
|
|
27
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Normalize both the legacy `{ "package-name": { ...config } }` form and
|
|
32
|
+
* the named instance form `{ "instance-id": { module, config } }`.
|
|
33
|
+
*/
|
|
34
|
+
getPluginEntry(pluginId) {
|
|
35
|
+
const plugins = this.config?.plugins;
|
|
36
|
+
if (!plugins || typeof plugins !== 'object')
|
|
37
|
+
return undefined;
|
|
38
|
+
const configKey = Object.prototype.hasOwnProperty.call(plugins, pluginId)
|
|
39
|
+
? pluginId
|
|
40
|
+
: Object.prototype.hasOwnProperty.call(plugins, `~${pluginId}`)
|
|
41
|
+
? `~${pluginId}`
|
|
42
|
+
: undefined;
|
|
43
|
+
if (!configKey)
|
|
44
|
+
return undefined;
|
|
45
|
+
const enabled = !configKey.startsWith('~');
|
|
46
|
+
const id = enabled ? configKey : configKey.substring(1);
|
|
47
|
+
const value = plugins[configKey];
|
|
48
|
+
const isInstance = value && typeof value === 'object' && !Array.isArray(value)
|
|
49
|
+
&& typeof value.module === 'string';
|
|
50
|
+
return {
|
|
51
|
+
id,
|
|
52
|
+
moduleName: isInstance ? value.module : id,
|
|
53
|
+
config: isInstance ? (value.config ?? {}) : (value ?? {}),
|
|
54
|
+
configKey,
|
|
55
|
+
enabled,
|
|
56
|
+
legacy: !isInstance,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
setPluginConfig(pluginId, config) {
|
|
60
|
+
const entry = this.getPluginEntry(pluginId);
|
|
61
|
+
if (!entry)
|
|
62
|
+
return false;
|
|
63
|
+
if (entry.legacy) {
|
|
64
|
+
this.config.plugins[entry.configKey] = config;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.config.plugins[entry.configKey].config = config;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
getEnabledPluginEntries() {
|
|
72
|
+
if (!this.config || typeof this.config.plugins !== 'object' || this.config.plugins === null) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
return Object.keys(this.config.plugins)
|
|
76
|
+
.map(key => this.getPluginEntry(key.startsWith('~') ? key.substring(1) : key))
|
|
77
|
+
.filter(entry => entry && entry.enabled);
|
|
78
|
+
}
|
|
28
79
|
/**
|
|
29
80
|
* Reloads the config file from disk into memory and emits a 'config-reloaded' event.
|
|
30
81
|
* This does NOT reload any plugins.
|
|
@@ -84,18 +135,14 @@ export class PluginLoader {
|
|
|
84
135
|
this.logger.info('No plugins configuration found. No plugins to load.');
|
|
85
136
|
return;
|
|
86
137
|
}
|
|
87
|
-
const
|
|
138
|
+
const allEntries = Object.keys(this.config.plugins)
|
|
139
|
+
.map(key => this.getPluginEntry(key.startsWith('~') ? key.substring(1) : key))
|
|
140
|
+
.filter(Boolean);
|
|
88
141
|
this.pluginStatus = {}; // Reset status
|
|
89
|
-
for (const
|
|
90
|
-
|
|
91
|
-
const actualName = name.substring(1);
|
|
92
|
-
this.pluginStatus[actualName] = "disabled" /* PluginStatus.DISABLED */;
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
this.pluginStatus[name] = "pending" /* PluginStatus.PENDING */;
|
|
96
|
-
}
|
|
142
|
+
for (const entry of allEntries) {
|
|
143
|
+
this.pluginStatus[entry.id] = entry.enabled ? "pending" /* PluginStatus.PENDING */ : "disabled" /* PluginStatus.DISABLED */;
|
|
97
144
|
}
|
|
98
|
-
const enabledPlugins =
|
|
145
|
+
const enabledPlugins = allEntries.filter(entry => entry.enabled).map(entry => entry.id);
|
|
99
146
|
const currentlyLoaded = Object.keys(this.plugins);
|
|
100
147
|
for (const loadedName of currentlyLoaded) {
|
|
101
148
|
if (!enabledPlugins.includes(loadedName)) {
|
|
@@ -170,13 +217,13 @@ export class PluginLoader {
|
|
|
170
217
|
return false;
|
|
171
218
|
}
|
|
172
219
|
this.plugins[pluginName] = pluginInstance;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
220
|
+
const entry = this.getPluginEntry(pluginName);
|
|
221
|
+
if (!entry) {
|
|
222
|
+
throw new Error(`Plugin configuration not found for instance "${pluginName}".`);
|
|
223
|
+
}
|
|
224
|
+
const schema = pluginInstance.config || Schema.object({});
|
|
225
|
+
const finalConfig = fallback(schema, entry.config);
|
|
226
|
+
this.setPluginConfig(pluginName, finalConfig);
|
|
180
227
|
const context = this.getContext(pluginName, {});
|
|
181
228
|
if (pluginInstance.render) {
|
|
182
229
|
context.renderer = this.core.renderers.get(pluginInstance.render || '');
|
|
@@ -188,14 +235,15 @@ export class PluginLoader {
|
|
|
188
235
|
}
|
|
189
236
|
if (this.isDev) {
|
|
190
237
|
let pluginPathToWatch = null;
|
|
238
|
+
const moduleName = entry.moduleName;
|
|
191
239
|
try {
|
|
192
|
-
const packageJsonUrl = import.meta.resolve(`${
|
|
240
|
+
const packageJsonUrl = import.meta.resolve(`${moduleName}/package.json`);
|
|
193
241
|
const pkgJsonPath = fileURLToPath(packageJsonUrl);
|
|
194
242
|
pluginPathToWatch = path.dirname(pkgJsonPath);
|
|
195
243
|
}
|
|
196
244
|
catch (e) {
|
|
197
|
-
const localPluginPath = path.resolve(process.cwd(),
|
|
198
|
-
const localPluginPathInPlugins = path.resolve(process.cwd(), 'plugins',
|
|
245
|
+
const localPluginPath = path.resolve(process.cwd(), moduleName);
|
|
246
|
+
const localPluginPathInPlugins = path.resolve(process.cwd(), 'plugins', moduleName);
|
|
199
247
|
if (fs.existsSync(localPluginPath)) {
|
|
200
248
|
pluginPathToWatch = localPluginPath;
|
|
201
249
|
}
|
|
@@ -314,29 +362,99 @@ export class PluginLoader {
|
|
|
314
362
|
});
|
|
315
363
|
this.pluginWatchers[pluginName] = watcher;
|
|
316
364
|
}
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return
|
|
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);
|
|
322
370
|
}
|
|
323
|
-
|
|
324
|
-
|
|
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())));
|
|
325
378
|
}
|
|
326
|
-
|
|
379
|
+
detectPackageManager() {
|
|
380
|
+
const cwd = process.cwd();
|
|
327
381
|
try {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
+
}
|
|
333
388
|
}
|
|
334
|
-
|
|
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);
|
|
335
434
|
}
|
|
336
435
|
catch (error) {
|
|
337
|
-
this.
|
|
338
|
-
|
|
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);
|
|
339
443
|
}
|
|
340
444
|
}
|
|
445
|
+
async loadModule(pluginName) {
|
|
446
|
+
const entry = this.getPluginEntry(pluginName);
|
|
447
|
+
if (!entry)
|
|
448
|
+
throw new Error(`Plugin configuration not found for instance "${pluginName}".`);
|
|
449
|
+
const pluginModule = await this.importPluginModule(entry.moduleName);
|
|
450
|
+
const context = this.getContext(pluginName, {});
|
|
451
|
+
return resolvePluginModule(pluginModule, context, entry.config);
|
|
452
|
+
}
|
|
453
|
+
async checkPluginDependencies(pluginPath) {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
async installPluginDependencies(pluginName) {
|
|
457
|
+
await this.installMissingPlugin(pluginName);
|
|
458
|
+
}
|
|
341
459
|
}
|
|
342
460
|
export default PluginLoader;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yumerijs/loader",
|
|
3
|
-
"version": "3.
|
|
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",
|
|
@@ -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.
|
|
32
|
+
"@yumerijs/core": "^3.0.5",
|
|
33
33
|
"esbuild-register": "^3.6.0",
|
|
34
34
|
"typescript": "^6.0.3"
|
|
35
35
|
},
|
|
@@ -41,6 +41,6 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@yumerijs/core": "^3.0.
|
|
44
|
+
"@yumerijs/core": "^3.0.5"
|
|
45
45
|
}
|
|
46
46
|
}
|