From 8f1831ecea320d2397f5a2aff5339e0cff9f2324 Mon Sep 17 00:00:00 2001 From: chenos Date: Sun, 27 Aug 2023 19:20:46 +0800 Subject: [PATCH] feat: improve pm.add --- jest.config.js | 2 +- packages/core/cli/src/commands/postinstall.js | 7 +- packages/core/cli/src/util.js | 55 ++++++- packages/core/database/src/repository.ts | 24 ++-- packages/core/server/package.json | 1 + .../plugin-manager-repository.ts | 25 +++- .../src/plugin-manager/plugin-manager.ts | 70 +++++++-- .../src/plugin-manager/plugin-resolver.ts | 135 ++++++++++++++++++ packages/core/server/src/plugin.ts | 15 +- storage/.gitignore | 3 +- 10 files changed, 294 insertions(+), 43 deletions(-) create mode 100644 packages/core/server/src/plugin-manager/plugin-resolver.ts diff --git a/jest.config.js b/jest.config.js index c3dcc996a9..59329a0057 100644 --- a/jest.config.js +++ b/jest.config.js @@ -25,7 +25,7 @@ module.exports = { }, ], }, - modulePathIgnorePatterns: ['/esm/', '/es/', '/dist/', '/lib/', '/client/', '/sdk/', '\\.test\\.tsx$'], + modulePathIgnorePatterns: ['/storage/', '/esm/', '/es/', '/dist/', '/lib/', '/client/', '/sdk/', '\\.test\\.tsx$'], coveragePathIgnorePatterns: [ '/node_modules/', '/__tests__/', diff --git a/packages/core/cli/src/commands/postinstall.js b/packages/core/cli/src/commands/postinstall.js index 804004c3a0..a66dfbda90 100644 --- a/packages/core/cli/src/commands/postinstall.js +++ b/packages/core/cli/src/commands/postinstall.js @@ -1,6 +1,6 @@ const { Command } = require('commander'); -const { run, isDev, isPackageValid } = require('../util'); -const { resolve } = require('path'); +const { run, isDev, isPackageValid, createStoragePluginsSymlink } = require('../util'); +const { resolve, dirname } = require('path'); const { existsSync } = require('fs'); const { readFile, writeFile } = require('fs').promises; @@ -13,10 +13,11 @@ module.exports = (cli) => { .command('postinstall') .allowUnknownOption() .action(async () => { + const cwd = process.cwd(); + await createStoragePluginsSymlink(); if (!isDev()) { return; } - const cwd = process.cwd(); if (!existsSync(resolve(cwd, '.env')) && existsSync(resolve(cwd, '.env.example'))) { const content = await readFile(resolve(cwd, '.env.example'), 'utf-8'); await writeFile(resolve(cwd, '.env'), content, 'utf-8'); diff --git a/packages/core/cli/src/util.js b/packages/core/cli/src/util.js index 4ba866ebf7..e925e21264 100644 --- a/packages/core/cli/src/util.js +++ b/packages/core/cli/src/util.js @@ -2,8 +2,8 @@ const net = require('net'); const chalk = require('chalk'); const execa = require('execa'); const { dirname, resolve } = require('path'); -const { readFile, writeFile } = require('fs').promises; const { existsSync, mkdirSync, cpSync } = require('fs'); +const { readFile, writeFile, readdir, symlink, unlink, mkdir, stat } = require('fs').promises; exports.isPackageValid = (package) => { try { @@ -174,3 +174,56 @@ exports.generateAppDir = function generateAppDir() { process.env.APP_PACKAGE_ROOT = appPkgPath; } }; + +async function getStoragePluginNames(target) { + const plugins = []; + const items = await readdir(target); + for (const item of items) { + if (item.startsWith('@')) { + const children = await getStoragePluginNames(resolve(target, item)); + plugins.push( + ...children.map((child) => { + return `${item}/${child}`; + }), + ); + } else if (await fsExists(resolve(target, item, 'package.json'))) { + plugins.push(item); + } + } + return plugins; +} + +async function fsExists(path) { + try { + await stat(path); + return true; + } catch (error) { + return false; + } +} + +async function createStoragePluginSymLink(pluginName) { + const storagePluginsPath = resolve(process.cwd(), 'storage/plugins'); + // const nodeModulesPath = resolve(dirname(require.resolve('@nocobase/server/package.json')), 'node_modules'); + const nodeModulesPath = resolve(process.cwd(), 'node_modules'); + if (pluginName.startsWith('@')) { + const [orgName] = pluginName.split('/'); + if (!(await fsExists(resolve(nodeModulesPath, orgName)))) { + await mkdir(resolve(nodeModulesPath, orgName), { recursive: true }); + } + } + const link = resolve(nodeModulesPath, pluginName); + if (await fsExists(link)) { + await unlink(link); + } + await symlink(resolve(storagePluginsPath, pluginName), link); +} + +async function createStoragePluginsSymlink() { + const pluginNames = await getStoragePluginNames(resolve(process.cwd(), 'storage/plugins')); + for (const pluginName of pluginNames) { + await createStoragePluginSymLink(pluginName); + } +} + +exports.createStoragePluginsSymlink = createStoragePluginsSymlink; diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index 7f32151801..b0a32ac10d 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -3,16 +3,16 @@ import lodash from 'lodash'; import { Association, BulkCreateOptions, - CountOptions as SequelizeCountOptions, - CreateOptions as SequelizeCreateOptions, - DestroyOptions as SequelizeDestroyOptions, - FindAndCountOptions as SequelizeAndCountOptions, - FindOptions as SequelizeFindOptions, ModelStatic, Op, Sequelize, - Transactionable, + FindAndCountOptions as SequelizeAndCountOptions, + CountOptions as SequelizeCountOptions, + CreateOptions as SequelizeCreateOptions, + DestroyOptions as SequelizeDestroyOptions, + FindOptions as SequelizeFindOptions, UpdateOptions as SequelizeUpdateOptions, + Transactionable, WhereOperators, } from 'sequelize'; import { Collection } from './collection'; @@ -37,6 +37,7 @@ import { UpdateGuard } from './update-guard'; const debug = require('debug')('noco-database'); +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface IRepository {} interface CreateManyOptions extends BulkCreateOptions { @@ -216,7 +217,12 @@ export interface AggregateOptions { distinct?: boolean; } -interface FirstOrCreateOptions extends Transactionable { +export interface FirstOrCreateOptions extends Transactionable { + filterKeys: string[]; + values?: Values; +} + +export interface UpdateOrCreateOptions extends Transactionable { filterKeys: string[]; values?: Values; } @@ -242,7 +248,7 @@ export class Repository { - return !Boolean(chunk.match(/\d+/)); + return !chunk.match(/\d+/); }) .join('.'); }; @@ -503,7 +509,7 @@ export class Repository(); - pluginAliases = new Map(); server: net.Server; + collection: Collection; + protected pluginInstances = new Map(); + protected pluginAliases = new Map(); + protected PluginResolver: typeof PluginResolver; + protected _repository: PluginManagerRepository; constructor(public options: PluginManagerOptions) { + this.PluginResolver = PluginResolver; this.app = options.app; this.app.db.registerRepositories({ PluginManagerRepository, @@ -75,6 +98,8 @@ export class PluginManager { } static getPackageJson(packageName: string) { + const file = require.resolve(`${packageName}/package.json`); + delete require.cache[file]; return require(`${packageName}/package.json`); } @@ -121,6 +146,16 @@ export class PluginManager { throw new Error(`No available packages found, ${name} plugin does not exist`); } + setPluginResolver(Resolver: typeof PluginResolver) { + this.PluginResolver = Resolver; + } + + async resolvePlugin(pluginName: AddingPluginType, options) { + const Resolver = this.PluginResolver; + const resolver = new Resolver(this.app); + return resolver.handle(pluginName, options); + } + static resolvePlugin(pluginName: string | typeof Plugin) { if (typeof pluginName === 'string') { const packageName = this.getPackageName(pluginName); @@ -189,7 +224,7 @@ export class PluginManager { await run('yarn', ['install']); } - async add(plugin?: any, options: any = {}) { + async add(plugin?: AddingPluginType, options: PMAddOptions = {}) { if (this.has(plugin)) { const name = typeof plugin === 'string' ? plugin : plugin.name; this.app.log.warn(`plugin [${name}] added`); @@ -199,19 +234,24 @@ export class PluginManager { options.name = plugin; } this.app.log.debug(`adding plugin [${options.name}]...`); - let P: any; try { - P = PluginManager.resolvePlugin(plugin); + const { Plugin: P, ...opts } = await this.resolvePlugin(plugin, options); + const instance: Plugin = new P(createAppProxy(this.app), opts); + this.pluginInstances.set(P, instance); + if (!options.isPreset && opts.name) { + await this.repository.updateOrCreate({ + values: { + ...options, + ...opts, + }, + filterKeys: ['name'], + }); + this.pluginAliases.set(options.name, instance); + } + await instance.afterAdd(); } catch (error) { this.app.log.warn('plugin not found', error); - return; } - const instance: Plugin = new P(createAppProxy(this.app), options); - this.pluginInstances.set(P, instance); - if (options.name) { - this.pluginAliases.set(options.name, instance); - } - await instance.afterAdd(); } async initPlugins() { diff --git a/packages/core/server/src/plugin-manager/plugin-resolver.ts b/packages/core/server/src/plugin-manager/plugin-resolver.ts new file mode 100644 index 0000000000..c1754125b4 --- /dev/null +++ b/packages/core/server/src/plugin-manager/plugin-resolver.ts @@ -0,0 +1,135 @@ +import { CleanOptions, SyncOptions } from '@nocobase/database'; +import { requireModule } from '@nocobase/utils'; +import Application from '../application'; +import { Plugin } from '../plugin'; +import { PluginManager } from './plugin-manager'; + +export interface PluginManagerOptions { + app: Application; + plugins?: any[]; +} + +export interface InstallOptions { + cliArgs?: any[]; + clean?: CleanOptions | boolean; + sync?: SyncOptions; +} + +export type PluginResolverResult = { + name?: string; + packageName?: string; + version?: string; + Plugin: any; +}; + +export type PluginResolverHandleOptions = { + isPackageName?: boolean; + pluginName?: string; + packageName?: string; + authToken?: string; + registry?: string; + upgrading?: boolean; +}; + +export type PluginType = string | typeof Plugin; + +export class PluginResolver { + constructor(protected app: Application) {} + + async handlePluginClass(pluginName: PluginType, options: PluginResolverHandleOptions): Promise { + return { + Plugin: pluginName, + }; + } + + async handleLocalPackage(pluginName: string, options: PluginResolverHandleOptions): Promise { + const { packageName } = options; + const packageJson = PluginManager.getPackageJson(packageName); + return { + name: pluginName, + packageName, + version: packageJson.version, + Plugin: requireModule(packageName), + }; + } + + async handleCompressedPackage(pkgPath: string, options: PluginResolverHandleOptions): Promise { + return { + Plugin: '', + }; + } + + async handle(pluginName: PluginType, options: PluginResolverHandleOptions): Promise { + if (typeof pluginName !== 'string') { + return this.handlePluginClass(pluginName, options); + } + + if (!options.isPackageName) { + options.packageName = PluginManager.getPackageName(pluginName); + return this.handleLocalPackage(pluginName, options); + } + + let pkgPath = pluginName; + + if (options.isPackageName) { + if (options.upgrading && !this.isStoragePackage(pluginName)) { + options.packageName = pluginName; + return this.handleLocalPackage(this.getPluginNameViaPackageName(pluginName), options); + } + if (this.packageExists(pluginName)) { + options.packageName = pluginName; + return this.handleLocalPackage(this.getPluginNameViaPackageName(pluginName), options); + } + pkgPath = await this.downloadNpmPackage(pluginName, options); + } else if (this.isURL(pluginName)) { + pkgPath = await this.download(pluginName, options); + } + + if (!this.isCompressedPackage(pkgPath)) { + throw new Error('invalid'); + } + + return this.handleCompressedPackage(pkgPath, options); + } + + getPluginNameViaPackageName(packageName: string) { + return packageName; + } + + protected async download(url: string, options) { + return ''; + } + + protected async downloadNpmPackage(packageName: string, options) { + return ''; + } + + isStoragePackage(packageName: string) { + return true; + } + + packageExists(packageName: string) { + try { + require.resolve(packageName); + return true; + } catch (error) { + return false; + } + } + + protected isCompressedPackage(file) { + return true; + } + + protected isURL(string) { + let url; + + try { + url = new URL(string); + } catch (e) { + return false; + } + + return url.protocol === 'http:' || url.protocol === 'https:'; + } +} diff --git a/packages/core/server/src/plugin.ts b/packages/core/server/src/plugin.ts index 51aabf19de..c72d2b8359 100644 --- a/packages/core/server/src/plugin.ts +++ b/packages/core/server/src/plugin.ts @@ -11,25 +11,22 @@ export interface PluginInterface { } export interface PluginOptions { - activate?: boolean; - displayName?: string; - description?: string; + name?: string; + packageName?: string; version?: string; enabled?: boolean; - install?: (this: Plugin) => void; - load?: (this: Plugin) => void; - plugin?: typeof Plugin; - + installed?: boolean; + isPreset?: boolean; [key: string]: any; } -export abstract class Plugin implements PluginInterface { +export abstract class Plugin implements PluginInterface { options: any; app: Application; model: Model; state: any = {}; - constructor(app: Application, options?: any) { + constructor(app: Application, options?: PluginOptions) { this.app = app; this.setOptions(options); } diff --git a/storage/.gitignore b/storage/.gitignore index bde6210e96..32531bec4c 100644 --- a/storage/.gitignore +++ b/storage/.gitignore @@ -1 +1,2 @@ -.pm2 \ No newline at end of file +.pm2 +plugins