mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-06 05:59:25 +08:00
439 lines
12 KiB
TypeScript
439 lines
12 KiB
TypeScript
import { CleanOptions, Collection, SyncOptions } from '@nocobase/database';
|
|
import { requireModule } from '@nocobase/utils';
|
|
import execa from 'execa';
|
|
import fs from 'fs';
|
|
import net from 'net';
|
|
import { resolve } from 'path';
|
|
import xpipe from 'xpipe';
|
|
import Application from '../application';
|
|
import { Plugin } from '../plugin';
|
|
import collectionOptions from './options/collection';
|
|
import resourceOptions from './options/resource';
|
|
import { PluginManagerRepository } from './plugin-manager-repository';
|
|
|
|
export interface PluginManagerOptions {
|
|
app: Application;
|
|
plugins?: any[];
|
|
}
|
|
|
|
export interface InstallOptions {
|
|
cliArgs?: any[];
|
|
clean?: CleanOptions | boolean;
|
|
sync?: SyncOptions;
|
|
}
|
|
|
|
export class PluginManager {
|
|
app: Application;
|
|
collection: Collection;
|
|
repository: PluginManagerRepository;
|
|
plugins = new Map<string, Plugin>();
|
|
server: net.Server;
|
|
pmSock: string;
|
|
_tmpPluginArgs = [];
|
|
|
|
constructor(options: PluginManagerOptions) {
|
|
this.app = options.app;
|
|
const f = resolve(process.cwd(), 'storage', 'pm.sock');
|
|
this.pmSock = xpipe.eq(this.app.options.pmSock || f);
|
|
this.app.db.registerRepositories({
|
|
PluginManagerRepository,
|
|
});
|
|
|
|
this.collection = this.app.db.collection(collectionOptions);
|
|
|
|
this.repository = this.collection.repository as PluginManagerRepository;
|
|
this.repository.setPluginManager(this);
|
|
this.app.resourcer.define(resourceOptions);
|
|
|
|
this.app.resourcer.use(async (ctx, next) => {
|
|
await next();
|
|
const { resourceName, actionName } = ctx.action;
|
|
if (resourceName === 'applicationPlugins' && actionName === 'list') {
|
|
const lng = ctx.getCurrentLocale();
|
|
if (Array.isArray(ctx.body)) {
|
|
ctx.body = ctx.body.map((plugin) => {
|
|
const json = plugin.toJSON();
|
|
const packageName = PluginManager.getPackageName(json.name);
|
|
const packageJson = PluginManager.getPackageJson(packageName);
|
|
return {
|
|
displayName: packageJson[`displayName.${lng}`] || packageJson.displayName,
|
|
description: packageJson[`description.${lng}`] || packageJson.description,
|
|
...json,
|
|
};
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
this.app.acl.registerSnippet({
|
|
name: 'pm',
|
|
actions: ['pm:*', 'applicationPlugins:list'],
|
|
});
|
|
|
|
this.app.on('beforeLoad', async (app, options) => {
|
|
if (options?.method && ['install', 'upgrade'].includes(options.method)) {
|
|
await this.collection.sync();
|
|
}
|
|
|
|
const exists = await this.app.db.collectionExistsInDb('applicationPlugins');
|
|
|
|
if (!exists) {
|
|
this.app.log.warn(`applicationPlugins collection not exists in ${this.app.name}`);
|
|
return;
|
|
}
|
|
|
|
if (options?.method !== 'install' || options.reload) {
|
|
await this.repository.load();
|
|
}
|
|
});
|
|
|
|
this.app.on('beforeUpgrade', async () => {
|
|
await this.collection.sync();
|
|
});
|
|
|
|
this.addStaticMultiple(options.plugins);
|
|
}
|
|
|
|
addStaticMultiple(plugins: any) {
|
|
for (const plugin of plugins || []) {
|
|
if (typeof plugin == 'string') {
|
|
this.addStatic(plugin);
|
|
} else {
|
|
this.addStatic(...plugin);
|
|
}
|
|
}
|
|
}
|
|
|
|
getPlugins() {
|
|
return this.plugins;
|
|
}
|
|
|
|
get(name: string) {
|
|
return this.plugins.get(name);
|
|
}
|
|
|
|
has(name: string) {
|
|
return this.plugins.has(name);
|
|
}
|
|
|
|
clientWrite(data: any) {
|
|
const { method, plugins } = data;
|
|
if (method === 'create') {
|
|
try {
|
|
console.log(method, plugins);
|
|
this[method](plugins);
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
}
|
|
return;
|
|
}
|
|
const client = new net.Socket();
|
|
client.connect(this.pmSock, () => {
|
|
client.write(JSON.stringify(data));
|
|
client.end();
|
|
});
|
|
client.on('error', async () => {
|
|
try {
|
|
console.log(method, plugins);
|
|
await this[method](plugins);
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
}
|
|
});
|
|
}
|
|
|
|
async listen(): Promise<net.Server> {
|
|
this.server = net.createServer((socket) => {
|
|
socket.on('data', async (data) => {
|
|
const { method, plugins } = JSON.parse(data.toString());
|
|
try {
|
|
console.log(method, plugins);
|
|
await this[method](plugins);
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
}
|
|
});
|
|
socket.pipe(socket);
|
|
});
|
|
|
|
if (fs.existsSync(this.pmSock)) {
|
|
await fs.promises.unlink(this.pmSock);
|
|
}
|
|
return new Promise((resolve) => {
|
|
this.server.listen(this.pmSock, () => {
|
|
resolve(this.server);
|
|
});
|
|
});
|
|
}
|
|
|
|
async create(name: string | string[]) {
|
|
console.log('creating...');
|
|
const pluginNames = Array.isArray(name) ? name : [name];
|
|
const { run } = require('@nocobase/cli/src/util');
|
|
const createPlugin = async (name) => {
|
|
const { PluginGenerator } = require('@nocobase/cli/src/plugin-generator');
|
|
const generator = new PluginGenerator({
|
|
cwd: resolve(process.cwd(), name),
|
|
args: {},
|
|
context: {
|
|
name,
|
|
},
|
|
});
|
|
await generator.run();
|
|
};
|
|
await Promise.all(pluginNames.map((pluginName) => createPlugin(pluginName)));
|
|
await run('yarn', ['install']);
|
|
}
|
|
|
|
clone() {
|
|
const pm = new PluginManager({
|
|
app: this.app,
|
|
});
|
|
for (const arg of this._tmpPluginArgs) {
|
|
pm.addStatic(...arg);
|
|
}
|
|
return pm;
|
|
}
|
|
|
|
addStatic(plugin?: any, options?: any) {
|
|
if (!options?.async) {
|
|
this._tmpPluginArgs.push([plugin, options]);
|
|
}
|
|
|
|
let name: string;
|
|
if (typeof plugin === 'string') {
|
|
name = plugin;
|
|
plugin = PluginManager.resolvePlugin(plugin);
|
|
} else {
|
|
name = plugin.name;
|
|
if (!name) {
|
|
throw new Error(`plugin name invalid`);
|
|
}
|
|
}
|
|
|
|
const instance = new plugin(this.app, {
|
|
name,
|
|
enabled: true,
|
|
...options,
|
|
});
|
|
|
|
const pluginName = instance.getName();
|
|
|
|
if (this.plugins.has(pluginName)) {
|
|
throw new Error(`plugin name [${pluginName}] already exists`);
|
|
}
|
|
|
|
this.plugins.set(pluginName, instance);
|
|
return instance;
|
|
}
|
|
|
|
async generateClientFile(plugin: string, packageName: string) {
|
|
const file = resolve(
|
|
process.cwd(),
|
|
'packages',
|
|
process.env.APP_PACKAGE_ROOT || 'app',
|
|
'client/src/plugins',
|
|
`${plugin}.ts`,
|
|
);
|
|
if (!fs.existsSync(file)) {
|
|
try {
|
|
require.resolve(`${packageName}/client`);
|
|
await fs.promises.writeFile(file, `export { default } from '${packageName}/client';`);
|
|
const { run } = require('@nocobase/cli/src/util');
|
|
await run('yarn', ['nocobase', 'postinstall']);
|
|
} catch (error) {}
|
|
}
|
|
}
|
|
|
|
async add(plugin: any, options: any = {}, transaction?: any) {
|
|
if (Array.isArray(plugin)) {
|
|
const t = transaction || (await this.app.db.sequelize.transaction());
|
|
try {
|
|
const items = [];
|
|
|
|
for (const p of plugin) {
|
|
items.push(await this.add(p, options, t));
|
|
}
|
|
|
|
await t.commit();
|
|
return items;
|
|
} catch (error) {
|
|
await t.rollback();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const packageName = await PluginManager.findPackage(plugin);
|
|
|
|
await this.generateClientFile(plugin, packageName);
|
|
|
|
const instance = this.addStatic(plugin, {
|
|
...options,
|
|
async: true,
|
|
});
|
|
|
|
const model = await this.repository.findOne({
|
|
transaction,
|
|
filter: { name: plugin },
|
|
});
|
|
|
|
const packageJson = PluginManager.getPackageJson(packageName);
|
|
|
|
if (!model) {
|
|
const { enabled, builtIn, installed, ...others } = options;
|
|
await this.repository.create({
|
|
transaction,
|
|
values: {
|
|
name: plugin,
|
|
version: packageJson.version,
|
|
enabled: !!enabled,
|
|
builtIn: !!builtIn,
|
|
installed: !!installed,
|
|
options: {
|
|
...others,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
async load(options: any = {}) {
|
|
for (const [name, plugin] of this.plugins) {
|
|
if (!plugin.enabled) {
|
|
continue;
|
|
}
|
|
await plugin.beforeLoad();
|
|
}
|
|
|
|
for (const [name, plugin] of this.plugins) {
|
|
if (!plugin.enabled) {
|
|
continue;
|
|
}
|
|
await this.app.emitAsync('beforeLoadPlugin', plugin, options);
|
|
await plugin.load();
|
|
await this.app.emitAsync('afterLoadPlugin', plugin, options);
|
|
}
|
|
}
|
|
|
|
async install(options: InstallOptions = {}) {
|
|
for (const [name, plugin] of this.plugins) {
|
|
if (!plugin.enabled) {
|
|
continue;
|
|
}
|
|
await this.app.emitAsync('beforeInstallPlugin', plugin, options);
|
|
await plugin.install(options);
|
|
await this.app.emitAsync('afterInstallPlugin', plugin, options);
|
|
}
|
|
}
|
|
|
|
async enable(name: string | string[]) {
|
|
try {
|
|
const pluginNames = await this.repository.enable(name);
|
|
await this.app.reload();
|
|
|
|
await this.app.db.sync();
|
|
for (const pluginName of pluginNames) {
|
|
const plugin = this.app.getPlugin(pluginName);
|
|
if (!plugin) {
|
|
throw new Error(`${name} plugin does not exist`);
|
|
}
|
|
if (!plugin.options.installed) {
|
|
await plugin.install();
|
|
plugin.options.installed = true;
|
|
}
|
|
await plugin.afterEnable();
|
|
}
|
|
|
|
await this.app.emitAsync('afterEnablePlugin', name);
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async disable(name: string | string[]) {
|
|
try {
|
|
const pluginNames = await this.repository.disable(name);
|
|
await this.app.reload();
|
|
for (const pluginName of pluginNames) {
|
|
const plugin = this.app.getPlugin(pluginName);
|
|
if (!plugin) {
|
|
throw new Error(`${name} plugin does not exist`);
|
|
}
|
|
await plugin.afterDisable();
|
|
}
|
|
|
|
await this.app.emitAsync('afterDisablePlugin', name);
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async remove(name: string | string[]) {
|
|
const pluginNames = typeof name === 'string' ? [name] : name;
|
|
for (const pluginName of pluginNames) {
|
|
const plugin = this.app.getPlugin(pluginName);
|
|
if (!plugin) {
|
|
throw new Error(`${name} plugin does not exist`);
|
|
}
|
|
await plugin.remove();
|
|
}
|
|
await this.repository.remove(name);
|
|
this.app.reload();
|
|
}
|
|
|
|
static getPackageJson(packageName: string) {
|
|
return require(`${packageName}/package.json`);
|
|
}
|
|
|
|
static getPackageName(name: string) {
|
|
const prefixes = this.getPluginPkgPrefix();
|
|
for (const prefix of prefixes) {
|
|
try {
|
|
require.resolve(`${prefix}${name}`);
|
|
return `${prefix}${name}`;
|
|
} catch (error) {
|
|
continue;
|
|
}
|
|
}
|
|
throw new Error(`${name} plugin does not exist`);
|
|
}
|
|
|
|
static getPluginPkgPrefix() {
|
|
return (process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-').split(
|
|
',',
|
|
);
|
|
}
|
|
|
|
static async findPackage(name: string) {
|
|
try {
|
|
const packageName = this.getPackageName(name);
|
|
return packageName;
|
|
} catch (error) {
|
|
console.log(`\`${name}\` plugin not found locally`);
|
|
const prefixes = this.getPluginPkgPrefix();
|
|
for (const prefix of prefixes) {
|
|
try {
|
|
const packageName = `${prefix}${name}`;
|
|
console.log(`Try to find ${packageName}`);
|
|
await execa('npm', ['v', packageName, 'versions']);
|
|
console.log(`${packageName} downloading`);
|
|
await execa('yarn', ['add', packageName, '-W']);
|
|
console.log(`${packageName} downloaded`);
|
|
return packageName;
|
|
} catch (error) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
throw new Error(`No available packages found, ${name} plugin does not exist`);
|
|
}
|
|
|
|
static resolvePlugin(pluginName: string) {
|
|
const packageName = this.getPackageName(pluginName);
|
|
return requireModule(packageName);
|
|
}
|
|
}
|
|
|
|
export default PluginManager;
|