feat: simplify the process of adding and updating plugins (#5275)

* feat: auto download pro

* feat: improve code

* feat: improve code

* feat: improve code

* feat: improve code

* fix: test error

* fix: improve code

* fix: yarn install error

* fix: build error

* fix: generatePlugins

* fix: test error

* fix: download pro command

* fix: run error

* feat: version

* fix: require packageJson

* fix: improve code

* feat: improve code

* fix: improve code

* fix: test error

* fix: test error

* fix: improve code

* fix: removable

* fix: error

* fix: error
This commit is contained in:
chenos 2024-09-15 01:37:46 +08:00 committed by GitHub
parent 475be58aa7
commit d7dc8fa4cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 942 additions and 411 deletions

1
.gitignore vendored
View File

@ -32,6 +32,7 @@ storage/plugins
storage/tar storage/tar
storage/tmp storage/tmp
storage/app.watch.ts storage/app.watch.ts
storage/.upgrading
storage/logs-e2e storage/logs-e2e
storage/uploads-e2e storage/uploads-e2e
storage/.pm2-* storage/.pm2-*

View File

@ -1,4 +1,4 @@
import { getUmiConfig, IndexGenerator } from '@nocobase/devtools/umiConfig'; import { generatePlugins, getUmiConfig } from '@nocobase/devtools/umiConfig';
import path from 'path'; import path from 'path';
import { defineConfig } from 'umi'; import { defineConfig } from 'umi';
@ -8,17 +8,11 @@ process.env.MFSU_AD = 'none';
process.env.DID_YOU_KNOW = 'none'; process.env.DID_YOU_KNOW = 'none';
const pluginPrefix = (process.env.PLUGIN_PACKAGE_PREFIX || '').split(',').filter((item) => !item.includes('preset')); // 因为现在 preset 是直接引入的,所以不能忽略,如果以后 preset 也是动态插件的形式引入,那么这里可以去掉 const pluginPrefix = (process.env.PLUGIN_PACKAGE_PREFIX || '').split(',').filter((item) => !item.includes('preset')); // 因为现在 preset 是直接引入的,所以不能忽略,如果以后 preset 也是动态插件的形式引入,那么这里可以去掉
const pluginDirs = (process.env.PLUGIN_PATH || 'packages/plugins/,packages/samples/,packages/pro-plugins/')
.split(',').map(item => path.join(process.cwd(), item));
const outputPluginPath = path.join(__dirname, 'src', '.plugins');
const indexGenerator = new IndexGenerator(outputPluginPath, pluginDirs);
indexGenerator.generate();
const isDevCmd = !!process.env.IS_DEV_CMD; const isDevCmd = !!process.env.IS_DEV_CMD;
const appPublicPath = isDevCmd ? '/' : '{{env.APP_PUBLIC_PATH}}'; const appPublicPath = isDevCmd ? '/' : '{{env.APP_PUBLIC_PATH}}';
generatePlugins();
export default defineConfig({ export default defineConfig({
title: 'Loading...', title: 'Loading...',
devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false, devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false,

View File

@ -9,8 +9,12 @@
const chalk = require('chalk'); const chalk = require('chalk');
const { Command } = require('commander'); const { Command } = require('commander');
const { runAppCommand, runInstall, run, postCheck, nodeCheck, promptForTs } = require('../util'); const { generatePlugins, run, postCheck, nodeCheck, promptForTs } = require('../util');
const { getPortPromise } = require('portfinder'); const { getPortPromise } = require('portfinder');
const chokidar = require('chokidar');
const { uid } = require('@formily/shared');
const path = require('path');
const fs = require('fs');
/** /**
* *
@ -27,6 +31,25 @@ module.exports = (cli) => {
.option('--inspect [port]') .option('--inspect [port]')
.allowUnknownOption() .allowUnknownOption()
.action(async (opts) => { .action(async (opts) => {
const watcher = chokidar.watch('./storage/plugins/**/*', {
cwd: process.cwd(),
ignored: /(^|[\/\\])\../, // 忽略隐藏文件
persistent: true,
depth: 1, // 只监听第一层目录
});
watcher
.on('addDir', async (pathname) => {
generatePlugins();
const file = path.resolve(process.cwd(), 'storage/app.watch.ts');
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
})
.on('unlinkDir', async (pathname) => {
generatePlugins();
const file = path.resolve(process.cwd(), 'storage/app.watch.ts');
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
});
promptForTs(); promptForTs();
const { SERVER_TSCONFIG_PATH } = process.env; const { SERVER_TSCONFIG_PATH } = process.env;
process.env.IS_DEV_CMD = true; process.env.IS_DEV_CMD = true;

View File

@ -8,7 +8,7 @@
*/ */
const { Command } = require('commander'); const { Command } = require('commander');
const { run, isDev, isProd, promptForTs } = require('../util'); const { run, isDev, isProd, promptForTs, downloadPro } = require('../util');
/** /**
* *
@ -20,10 +20,14 @@ module.exports = (cli) => {
.allowUnknownOption() .allowUnknownOption()
.option('-h, --help') .option('-h, --help')
.option('--ts-node-dev') .option('--ts-node-dev')
.action((options) => { .action(async (options) => {
const cmd = process.argv.slice(2)?.[0];
if (cmd === 'install') {
await downloadPro();
}
if (isDev()) { if (isDev()) {
promptForTs(); promptForTs();
run('tsx', [ await run('tsx', [
'--tsconfig', '--tsconfig',
SERVER_TSCONFIG_PATH, SERVER_TSCONFIG_PATH,
'-r', '-r',
@ -32,7 +36,7 @@ module.exports = (cli) => {
...process.argv.slice(2), ...process.argv.slice(2),
]); ]);
} else if (isProd()) { } else if (isProd()) {
run('node', [`${APP_PACKAGE_ROOT}/lib/index.js`, ...process.argv.slice(2)]); await run('node', [`${APP_PACKAGE_ROOT}/lib/index.js`, ...process.argv.slice(2)]);
} }
}); });
}; };

View File

@ -31,6 +31,7 @@ module.exports = (cli) => {
require('./umi')(cli); require('./umi')(cli);
require('./upgrade')(cli); require('./upgrade')(cli);
require('./postinstall')(cli); require('./postinstall')(cli);
require('./pkg')(cli);
if (isPackageValid('@umijs/utils')) { if (isPackageValid('@umijs/utils')) {
require('./create-plugin')(cli); require('./create-plugin')(cli);
} }

View File

@ -0,0 +1,218 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
const { Command } = require('commander');
const axios = require('axios');
const fs = require('fs-extra');
const zlib = require('zlib');
const tar = require('tar');
const path = require('path');
const { createStoragePluginsSymlink } = require('@nocobase/utils/plugin-symlink');
const chalk = require('chalk');
class Package {
data;
constructor(packageName, packageManager) {
this.packageName = packageName;
this.packageManager = packageManager;
this.outputDir = path.resolve(process.cwd(), `storage/plugins/${this.packageName}`);
}
get token() {
return this.packageManager.getToken();
}
url(path) {
return this.packageManager.url(path);
}
async mkdir() {
if (await fs.exists(this.outputDir)) {
await fs.rm(this.outputDir, { recursive: true, force: true });
}
await fs.mkdirp(this.outputDir);
}
async getInfo() {
try {
const res = await axios.get(this.url(this.packageName), {
headers: {
Authorization: `Bearer ${this.token}`,
},
responseType: 'json',
});
this.data = res.data;
} catch (error) {
return;
}
}
getTarball(version = 'latest') {
if (this.data.versions[version]) {
return [version, this.data.versions[version].dist.tarball];
}
if (version.includes('beta')) {
version = version.split('beta')[0] + 'beta';
} else if (version.includes('alpha')) {
const prefix = (version = version.split('alpha')[0]);
version = Object.keys(this.data.versions)
.filter((ver) => ver.startsWith(`${prefix}alpha`))
.sort()
.pop();
}
if (version === 'latest') {
version = this.data['dist-tags']['latest'];
} else if (version === 'next') {
version = this.data['dist-tags']['next'];
}
if (!this.data.versions[version]) {
console.log(chalk.redBright(`Download failed: ${this.packageName}@${version} package does not exist`));
}
return [version, this.data.versions[version].dist.tarball];
}
async isDevPackage() {
let file = path.resolve(process.cwd(), 'packages/plugins', this.packageName, 'package.json');
if (await fs.exists(file)) {
return true;
}
file = path.resolve(process.cwd(), 'packages/pro-plugins', this.packageName, 'package.json');
if (await fs.exists(file)) {
return true;
}
return false;
}
async download(options = {}) {
if (await this.isDevPackage()) {
console.log(chalk.yellowBright(`Skipped: ${this.packageName} is dev package`));
return;
}
await this.getInfo();
if (!this.data) {
console.log(chalk.redBright(`Download failed: ${this.packageName} package does not exist`));
return;
}
try {
const [version, url] = this.getTarball(options.version);
const response = await axios({
url,
responseType: 'stream',
method: 'GET',
headers: {
Authorization: `Bearer ${this.token}`,
},
});
await this.mkdir();
await new Promise((resolve, reject) => {
response.data
.pipe(zlib.createGunzip()) // 解压 gzip
.pipe(tar.extract({ cwd: this.outputDir, strip: 1 })) // 解压 tar
.on('finish', resolve)
.on('error', reject);
});
console.log(chalk.greenBright(`Download success: ${this.packageName}@${version}`));
} catch (error) {
console.log(chalk.redBright(`Download failed: ${this.packageName}`));
}
}
}
class PackageManager {
token;
baseURL;
constructor({ baseURL }) {
this.baseURL = baseURL;
}
getToken() {
return this.token;
}
getBaseURL() {
return this.baseURL;
}
url(path) {
return this.baseURL + path;
}
async login(credentials) {
try {
const res1 = await axios.post(`${this.baseURL}-/verdaccio/sec/login`, credentials, {
responseType: 'json',
});
this.token = res1.data.token;
} catch (error) {
console.error(chalk.redBright(`Login failed: ${this.baseURL}`));
}
}
getPackage(packageName) {
return new Package(packageName, this);
}
async getProPackages() {
const res = await axios.get(this.url('pro-packages'), {
headers: {
Authorization: `Bearer ${this.token}`,
},
responseType: 'json',
});
return res.data.data;
}
async getPackages() {
const pkgs = await this.getProPackages();
return pkgs;
}
async download(options = {}) {
const { version } = options;
if (!this.token) {
return;
}
const pkgs = await this.getPackages();
for (const pkg of pkgs) {
await this.getPackage(pkg).download({ version });
}
}
}
/**
*
* @param {Command} cli
*/
module.exports = (cli) => {
const pkg = cli.command('pkg');
pkg
.command('download-pro')
.option('-V, --version [version]')
.action(async () => {
const { NOCOBASE_PKG_URL, NOCOBASE_PKG_USERNAME, NOCOBASE_PKG_PASSWORD } = process.env;
if (!(NOCOBASE_PKG_URL && NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD)) {
return;
}
const credentials = { username: NOCOBASE_PKG_USERNAME, password: NOCOBASE_PKG_PASSWORD };
const pm = new PackageManager({ baseURL: NOCOBASE_PKG_URL });
await pm.login(credentials);
const file = path.resolve(__dirname, '../../package.json');
const json = await fs.readJson(file);
await pm.download({ version: json.version });
await createStoragePluginsSymlink();
});
pkg.command('export-all').action(async () => {
console.log('Todo...');
});
};

View File

@ -8,7 +8,7 @@
*/ */
const { Command } = require('commander'); const { Command } = require('commander');
const { run, isDev, isPackageValid, generatePlaywrightPath } = require('../util'); const { run, isDev, isPackageValid, generatePlaywrightPath, generatePlugins } = require('../util');
const { dirname, resolve } = require('path'); const { dirname, resolve } = require('path');
const { existsSync, mkdirSync, readFileSync, appendFileSync } = require('fs'); const { existsSync, mkdirSync, readFileSync, appendFileSync } = require('fs');
const { readFile, writeFile } = require('fs').promises; const { readFile, writeFile } = require('fs').promises;
@ -41,7 +41,7 @@ module.exports = (cli) => {
.option('--skip-umi') .option('--skip-umi')
.action(async (options) => { .action(async (options) => {
writeToExclude(); writeToExclude();
generatePlugins();
generatePlaywrightPath(true); generatePlaywrightPath(true);
await createStoragePluginsSymlink(); await createStoragePluginsSymlink();
if (!isDev()) { if (!isDev()) {

View File

@ -8,10 +8,11 @@
*/ */
const { Command } = require('commander'); const { Command } = require('commander');
const { isDev, run, postCheck, runInstall, promptForTs } = require('../util'); const { isDev, run, postCheck, downloadPro, promptForTs } = require('../util');
const { existsSync, rmSync } = require('fs'); const { existsSync, rmSync } = require('fs');
const { resolve } = require('path'); const { resolve } = require('path');
const chalk = require('chalk'); const chalk = require('chalk');
const chokidar = require('chokidar');
function deleteSockFiles() { function deleteSockFiles() {
const { SOCKET_PATH, PM2_HOME } = process.env; const { SOCKET_PATH, PM2_HOME } = process.env;
@ -38,6 +39,23 @@ module.exports = (cli) => {
.option('--quickstart') .option('--quickstart')
.allowUnknownOption() .allowUnknownOption()
.action(async (opts) => { .action(async (opts) => {
if (opts.quickstart) {
await downloadPro();
}
const watcher = chokidar.watch('./storage/plugins/**/*', {
cwd: process.cwd(),
ignoreInitial: true,
ignored: /(^|[\/\\])\../, // 忽略隐藏文件
persistent: true,
depth: 1, // 只监听第一层目录
});
watcher.on('addDir', async (pathname) => {
console.log('pathname', pathname);
await run('yarn', ['nocobase', 'pm2-restart']);
});
if (opts.port) { if (opts.port) {
process.env.APP_PORT = opts.port; process.env.APP_PORT = opts.port;
} }

View File

@ -10,7 +10,7 @@
const chalk = require('chalk'); const chalk = require('chalk');
const { Command } = require('commander'); const { Command } = require('commander');
const { resolve } = require('path'); const { resolve } = require('path');
const { run, promptForTs, runAppCommand, hasCorePackages, updateJsonFile, hasTsNode } = require('../util'); const { run, promptForTs, runAppCommand, hasCorePackages, downloadPro, hasTsNode } = require('../util');
const { existsSync, rmSync } = require('fs'); const { existsSync, rmSync } = require('fs');
/** /**
@ -29,15 +29,18 @@ module.exports = (cli) => {
if (hasTsNode()) promptForTs(); if (hasTsNode()) promptForTs();
if (hasCorePackages()) { if (hasCorePackages()) {
// await run('yarn', ['install']); // await run('yarn', ['install']);
await downloadPro();
await runAppCommand('upgrade'); await runAppCommand('upgrade');
return; return;
} }
if (options.skipCodeUpdate) { if (options.skipCodeUpdate) {
await downloadPro();
await runAppCommand('upgrade'); await runAppCommand('upgrade');
return; return;
} }
// await runAppCommand('upgrade'); // await runAppCommand('upgrade');
if (!hasTsNode()) { if (!hasTsNode()) {
await downloadPro();
await runAppCommand('upgrade'); await runAppCommand('upgrade');
return; return;
} }
@ -54,8 +57,9 @@ module.exports = (cli) => {
stdio: 'pipe', stdio: 'pipe',
}); });
if (pkg.version === stdout) { if (pkg.version === stdout) {
await downloadPro();
await runAppCommand('upgrade'); await runAppCommand('upgrade');
rmAppDir(); await rmAppDir();
return; return;
} }
const currentY = 1 * pkg.version.split('.')[1]; const currentY = 1 * pkg.version.split('.')[1];
@ -66,7 +70,8 @@ module.exports = (cli) => {
await run('yarn', ['add', '@nocobase/cli', '@nocobase/devtools', '-W']); await run('yarn', ['add', '@nocobase/cli', '@nocobase/devtools', '-W']);
} }
await run('yarn', ['install']); await run('yarn', ['install']);
await downloadPro();
await runAppCommand('upgrade'); await runAppCommand('upgrade');
rmAppDir(); await rmAppDir();
}); });
}; };

View File

@ -72,7 +72,7 @@ class PluginGenerator extends Generator {
}); });
this.log(''); this.log('');
genTsConfigPaths(); genTsConfigPaths();
execa.sync('yarn', ['postinstall', '--skip-umi'], { shell: true, stdio: 'inherit' }); execa.sync('yarn', ['postinstall'], { shell: true, stdio: 'inherit' });
this.log(`The plugin folder is in ${chalk.green(`packages/plugins/${name}`)}`); this.log(`The plugin folder is in ${chalk.green(`packages/plugins/${name}`)}`);
} }
} }

View File

@ -163,6 +163,10 @@ exports.promptForTs = () => {
console.log(chalk.green('WAIT: ') + 'TypeScript compiling...'); console.log(chalk.green('WAIT: ') + 'TypeScript compiling...');
}; };
exports.downloadPro = async () => {
await exports.run('yarn', ['nocobase', 'pkg', 'download-pro']);
};
exports.updateJsonFile = async (target, fn) => { exports.updateJsonFile = async (target, fn) => {
const content = await readFile(target, 'utf-8'); const content = await readFile(target, 'utf-8');
const json = JSON.parse(content); const json = JSON.parse(content);
@ -416,3 +420,13 @@ exports.initEnv = function initEnv() {
); );
} }
}; };
exports.generatePlugins = function () {
try {
require.resolve('@nocobase/devtools/umiConfig');
const { generatePlugins } = require('@nocobase/devtools/umiConfig');
generatePlugins();
} catch (error) {
return;
}
};

View File

@ -135,7 +135,9 @@ export class APIClient extends APIClientSDK {
}, },
async (error) => { async (error) => {
if (this.silence) { if (this.silence) {
throw error; console.error(error);
return;
// throw error;
} }
const redirectTo = error?.response?.data?.redirectTo; const redirectTo = error?.response?.data?.redirectTo;
if (redirectTo) { if (redirectTo) {

View File

@ -7,14 +7,13 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { App, Card, Divider, Popconfirm, Space, Switch, Typography } from 'antd'; import { DeleteOutlined, LoadingOutlined, ReadOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { App, Card, Divider, Modal, Popconfirm, Result, Space, Switch, Typography } from 'antd';
import classnames from 'classnames'; import classnames from 'classnames';
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { DeleteOutlined, ReadOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { useAPIClient } from '../api-client'; import { useAPIClient } from '../api-client';
import { useApp } from '../application'; import { useApp } from '../application';
import { PluginDetail } from './PluginDetail'; import { PluginDetail } from './PluginDetail';
@ -29,8 +28,19 @@ interface IPluginInfo extends IPluginCard {
function PluginInfo(props: IPluginInfo) { function PluginInfo(props: IPluginInfo) {
const { data, onClick } = props; const { data, onClick } = props;
const app = useApp(); const app = useApp();
const { name, displayName, isCompatible, packageName, updatable, builtIn, enabled, description, error, homepage } = const {
data; name,
displayName,
isCompatible,
packageName,
updatable,
builtIn,
enabled,
removable,
description,
error,
homepage,
} = data;
const { styles, theme } = useStyles(); const { styles, theme } = useStyles();
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
@ -109,30 +119,58 @@ function PluginInfo(props: IPluginInfo) {
<ReloadOutlined /> {t('Update')} <ReloadOutlined /> {t('Update')}
</a> </a>
)} )}
{enabled ? ( {enabled && app.pluginSettingsManager.has(name) && (
app.pluginSettingsManager.has(name) && ( <a
<a onClick={(e) => {
onClick={(e) => { e.stopPropagation();
e.stopPropagation(); navigate(app.pluginSettingsManager.getRoutePath(name));
navigate(app.pluginSettingsManager.getRoutePath(name)); }}
}} >
> <SettingOutlined /> {t('Settings')}
<SettingOutlined /> {t('Settings')} </a>
</a> )}
) {removable && (
) : (
<Popconfirm <Popconfirm
key={'delete'} key={'delete'}
disabled={builtIn} disabled={builtIn}
title={t('Are you sure to delete this plugin?')} title={t('Are you sure to delete this plugin?')}
onConfirm={async (e) => { onConfirm={async (e) => {
e.stopPropagation(); e.stopPropagation();
api.request({ await api.request({
url: `pm:remove`, url: `pm:remove`,
params: { params: {
filterByTk: name, filterByTk: name,
}, },
}); });
Modal.info({
icon: null,
width: 520,
content: (
<Result
icon={<LoadingOutlined />}
title={t('Plugin removing')}
subTitle={t('Plugin is removing, please wait...')}
/>
),
footer: null,
});
function __health_check() {
api
.silent()
.request({
url: `__health_check`,
method: 'get',
})
.then((response) => {
if (response?.data === 'ok') {
window.location.reload();
}
})
.catch((error) => {
// console.error('Health check failed:', error);
});
}
setInterval(__health_check, 1000);
}} }}
onCancel={(e) => e.stopPropagation()} onCancel={(e) => e.stopPropagation()}
okText={t('Yes')} okText={t('Yes')}
@ -215,34 +253,6 @@ function PluginInfo(props: IPluginInfo) {
) )
} }
/> />
{/* {!isCompatible && !error && (
<Button style={{ padding: 0 }} type="link">
<Typography.Text type="danger">{t('Dependencies check failed')}</Typography.Text>
</Button>
)} */}
{/*
<Col span={8}>
<Space direction="vertical" align="end" style={{ display: 'flex', marginTop: -10 }}>
{type && (
<Button
onClick={(e) => {
e.stopPropagation();
setShowUploadForm(true);
}}
ghost
type="primary"
>
{t('Update plugin')}
</Button>
)}
{!error && (
<Button style={{ padding: 0 }} type="link">
{t('More details')}
</Button>
)}
</Space>
</Col> */}
</Card> </Card>
</> </>
); );

View File

@ -13,7 +13,6 @@ import relativeTime from 'dayjs/plugin/relativeTime';
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRequest } from '../api-client'; import { useRequest } from '../api-client';
import { PluginDocument } from './PluginDocument';
import { useStyles } from './style'; import { useStyles } from './style';
import { IPluginData } from './types'; import { IPluginData } from './types';
@ -121,7 +120,7 @@ export const PluginDetail: FC<IPluginDetail> = ({ plugin, onCancel }) => {
children: ( children: (
<Row gutter={20}> <Row gutter={20}>
{plugin.name && ( {plugin.name && (
<Col span={12}> <Col span={24}>
<div className={styles.PluginDetailBaseInfo}> <div className={styles.PluginDetailBaseInfo}>
<Typography.Text type="secondary">{t('Name')}</Typography.Text> <Typography.Text type="secondary">{t('Name')}</Typography.Text>
<Typography.Text strong>{plugin.name}</Typography.Text> <Typography.Text strong>{plugin.name}</Typography.Text>
@ -129,7 +128,7 @@ export const PluginDetail: FC<IPluginDetail> = ({ plugin, onCancel }) => {
</Col> </Col>
)} )}
{plugin.displayName && ( {plugin.displayName && (
<Col span={12}> <Col span={24}>
<div className={styles.PluginDetailBaseInfo}> <div className={styles.PluginDetailBaseInfo}>
<Typography.Text type="secondary">{t('DisplayName')}</Typography.Text> <Typography.Text type="secondary">{t('DisplayName')}</Typography.Text>
<Typography.Text strong>{plugin.displayName}</Typography.Text> <Typography.Text strong>{plugin.displayName}</Typography.Text>
@ -169,7 +168,7 @@ export const PluginDetail: FC<IPluginDetail> = ({ plugin, onCancel }) => {
</Col> </Col>
)} )}
{data?.data?.packageJson.license && ( {data?.data?.packageJson.license && (
<Col span={12}> <Col span={24}>
<div className={styles.PluginDetailBaseInfo}> <div className={styles.PluginDetailBaseInfo}>
<Typography.Text type="secondary">{t('License')}</Typography.Text> <Typography.Text type="secondary">{t('License')}</Typography.Text>
<Typography.Text strong>{data?.data?.packageJson.license}</Typography.Text> <Typography.Text strong>{data?.data?.packageJson.license}</Typography.Text>
@ -177,20 +176,14 @@ export const PluginDetail: FC<IPluginDetail> = ({ plugin, onCancel }) => {
</Col> </Col>
)} )}
{author && ( {author && (
<Col span={12}> <Col span={24}>
<div className={styles.PluginDetailBaseInfo}> <div className={styles.PluginDetailBaseInfo}>
<Typography.Text type="secondary">{t('Author')}</Typography.Text> <Typography.Text type="secondary">{t('Author')}</Typography.Text>
<Typography.Text strong>{author}</Typography.Text> <Typography.Text strong>{author}</Typography.Text>
</div> </div>
</Col> </Col>
)} )}
<Col span={12}> <Col span={24}>
<div className={styles.PluginDetailBaseInfo}>
<Typography.Text type="secondary">{t('Last updated')}</Typography.Text>
<Typography.Text strong>{dayjs(data?.data?.lastUpdated).fromNow()}</Typography.Text>
</div>
</Col>
<Col span={12}>
<div className={styles.PluginDetailBaseInfo}> <div className={styles.PluginDetailBaseInfo}>
<Typography.Text type="secondary">{t('Version')}</Typography.Text> <Typography.Text type="secondary">{t('Version')}</Typography.Text>
<Typography.Text strong>{plugin?.version}</Typography.Text> <Typography.Text strong>{plugin?.version}</Typography.Text>
@ -231,11 +224,11 @@ export const PluginDetail: FC<IPluginDetail> = ({ plugin, onCancel }) => {
</> </>
), ),
}, },
{ // {
key: 'changelog', // key: 'changelog',
label: t('Changelog'), // label: t('Changelog'),
children: plugin?.changelogUrl ? <PluginDocument url={plugin?.changelogUrl} /> : t('No CHANGELOG.md file'), // children: plugin?.changelogUrl ? <PluginDocument url={plugin?.changelogUrl} /> : t('No CHANGELOG.md file'),
}, // },
]; ];
return ( return (
@ -248,9 +241,6 @@ export const PluginDetail: FC<IPluginDetail> = ({ plugin, onCancel }) => {
<Typography.Title level={3}>{plugin.packageName}</Typography.Title> <Typography.Title level={3}>{plugin.packageName}</Typography.Title>
<Space split={<span>&nbsp;&nbsp;</span>}> <Space split={<span>&nbsp;&nbsp;</span>}>
<span>{plugin.version}</span> <span>{plugin.version}</span>
<span>
{t('Last updated')} {dayjs(data?.data?.lastUpdated).fromNow()}
</span>
</Space> </Space>
<Tabs <Tabs
style={{ minHeight: '50vh' }} style={{ minHeight: '50vh' }}

View File

@ -7,10 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { LoadingOutlined } from '@ant-design/icons';
import { ISchema } from '@formily/json-schema'; import { ISchema } from '@formily/json-schema';
import { useForm } from '@formily/react'; import { useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { App } from 'antd'; import { App, Modal, Result } from 'antd';
import type { RcFile } from 'antd/es/upload'; import type { RcFile } from 'antd/es/upload';
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -19,6 +20,8 @@ import { useAPIClient } from '../../../api-client';
import { SchemaComponent } from '../../../schema-component'; import { SchemaComponent } from '../../../schema-component';
import { IPluginData } from '../../types'; import { IPluginData } from '../../types';
const { confirm } = Modal;
interface IPluginUploadFormProps { interface IPluginUploadFormProps {
onClose: (refresh?: boolean) => void; onClose: (refresh?: boolean) => void;
isUpgrade: boolean; isUpgrade: boolean;
@ -40,12 +43,41 @@ export const PluginUploadForm: FC<IPluginUploadFormProps> = ({ onClose, pluginDa
if (pluginData?.packageName) { if (pluginData?.packageName) {
formData.append('packageName', pluginData.packageName); formData.append('packageName', pluginData.packageName);
} }
api.request({ await api.request({
url: `pm:${isUpgrade ? 'update' : 'add'}`, url: `pm:${isUpgrade ? 'update' : 'add'}`,
method: 'post', method: 'post',
data: formData, data: formData,
}); });
Modal.info({
icon: null,
width: 520,
content: (
<Result
icon={<LoadingOutlined />}
title={t('Plugin uploading')}
subTitle={t('Plugin is uploading, please wait...')}
/>
),
footer: null,
});
onClose(true); onClose(true);
function __health_check() {
api
.silent()
.request({
url: `__health_check`,
method: 'get',
})
.then((response) => {
if (response?.data === 'ok') {
window.location.reload();
}
})
.catch((error) => {
// console.error('Health check failed:', error);
});
}
setInterval(__health_check, 1000);
}, },
}; };
}; };

View File

@ -27,7 +27,7 @@ export const PluginAddModal: FC<IPluginFormProps> = ({ onClose, isShow }) => {
const [type, setType] = useState<'npm' | 'upload' | 'url'>('npm'); const [type, setType] = useState<'npm' | 'upload' | 'url'>('npm');
return ( return (
<Modal onCancel={() => onClose()} footer={null} destroyOnClose title={t('Add plugin')} width={580} open={isShow}> <Modal onCancel={() => onClose()} footer={null} destroyOnClose title={t('Add & update')} width={580} open={isShow}>
{/* <label style={{ fontWeight: 'bold' }}>{t('Source')}:</label> */} {/* <label style={{ fontWeight: 'bold' }}>{t('Source')}:</label> */}
<div style={{ marginTop: theme.marginLG, marginBottom: theme.marginLG }}> <div style={{ marginTop: theme.marginLG, marginBottom: theme.marginLG }}>
<Radio.Group optionType="button" defaultValue={type} onChange={(e) => setType(e.target.value)}> <Radio.Group optionType="button" defaultValue={type} onChange={(e) => setType(e.target.value)}>

View File

@ -197,7 +197,7 @@ const LocalPlugins = () => {
<div> <div>
<Space> <Space>
<Button onClick={() => setShowAddForm(true)} type="primary"> <Button onClick={() => setShowAddForm(true)} type="primary">
{t('Add new')} {t('Add & Update')}
</Button> </Button>
</Space> </Space>
</div> </div>

View File

@ -16,6 +16,7 @@ export interface IPluginData {
packageName: string; packageName: string;
version: string; version: string;
enabled: boolean; enabled: boolean;
removable?: boolean;
installed: boolean; installed: boolean;
builtIn: boolean; builtIn: boolean;
registry?: string; registry?: string;

View File

@ -24,3 +24,5 @@ export declare class IndexGenerator {
constructor(outputPath: string, pluginsPath: string[]): void; constructor(outputPath: string, pluginsPath: string[]): void;
generate(): void; generate(): void;
}; };
export declare function generatePlugins(): {}

View File

@ -137,14 +137,14 @@ class IndexGenerator {
generate() { generate() {
this.generatePluginContent(); this.generatePluginContent();
if (process.env.NODE_ENV === 'production') return; if (process.env.NODE_ENV === 'production') return;
this.pluginsPath.forEach((pluginPath) => { // this.pluginsPath.forEach((pluginPath) => {
if (!fs.existsSync(pluginPath)) { // if (!fs.existsSync(pluginPath)) {
return; // return;
} // }
fs.watch(pluginPath, { recursive: false }, () => { // fs.watch(pluginPath, { recursive: false }, () => {
this.generatePluginContent(); // this.generatePluginContent();
}); // });
}); // });
} }
get indexContent() { get indexContent() {
@ -156,7 +156,11 @@ function devDynamicImport(packageName: string): Promise<any> {
if (!fileName) { if (!fileName) {
return Promise.resolve(null); return Promise.resolve(null);
} }
return import(\`./packages/\${fileName}\`) try {
return import(\`./packages/\${fileName}\`)
} catch (error) {
return Promise.resolve(null);
}
} }
export default devDynamicImport;`; export default devDynamicImport;`;
} }
@ -170,9 +174,9 @@ export default function devDynamicImport(packageName: string): Promise<any> {
generatePluginContent() { generatePluginContent() {
if (fs.existsSync(this.outputPath)) { if (fs.existsSync(this.outputPath)) {
fs.rmdirSync(this.outputPath, { recursive: true, force: true }); fs.rmSync(this.outputPath, { recursive: true, force: true });
} }
fs.mkdirSync(this.outputPath); fs.mkdirSync(this.outputPath, { recursive: true, force: true });
const validPluginPaths = this.pluginsPath.filter((pluginsPath) => fs.existsSync(pluginsPath)); const validPluginPaths = this.pluginsPath.filter((pluginsPath) => fs.existsSync(pluginsPath));
if (!validPluginPaths.length || process.env.NODE_ENV === 'production') { if (!validPluginPaths.length || process.env.NODE_ENV === 'production') {
fs.writeFileSync(this.indexPath, this.emptyIndexContent); fs.writeFileSync(this.indexPath, this.emptyIndexContent);
@ -247,3 +251,13 @@ export default function devDynamicImport(packageName: string): Promise<any> {
exports.getUmiConfig = getUmiConfig; exports.getUmiConfig = getUmiConfig;
exports.resolveNocobasePackagesAlias = resolveNocobasePackagesAlias; exports.resolveNocobasePackagesAlias = resolveNocobasePackagesAlias;
exports.IndexGenerator = IndexGenerator; exports.IndexGenerator = IndexGenerator;
exports.generatePlugins = function () {
const pluginDirs = (process.env.PLUGIN_PATH || 'packages/plugins/,packages/samples/,packages/pro-plugins/')
.split(',')
.map((item) => path.join(process.cwd(), item));
const outputPluginPath = path.join(process.env.APP_PACKAGE_ROOT, 'client', 'src', '.plugins');
const indexGenerator = new IndexGenerator(outputPluginPath, pluginDirs);
indexGenerator.generate();
};

View File

@ -7,10 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { MockServer, mockServer } from '@nocobase/test';
import { vi } from 'vitest';
import Plugin from '../plugin'; import Plugin from '../plugin';
import { PluginManager } from '../plugin-manager'; import { PluginManager } from '../plugin-manager';
import { vi } from 'vitest';
import { MockServer, mockServer } from '@nocobase/test';
describe('pm', () => { describe('pm', () => {
let app: MockServer; let app: MockServer;
@ -216,7 +216,40 @@ describe('pm', () => {
PluginManager.resolvePlugin = resolvePlugin; PluginManager.resolvePlugin = resolvePlugin;
}); });
test('enable12', async () => { test('afterAdd + beforeLoad + load', async () => {
const resolvePlugin = PluginManager.resolvePlugin;
PluginManager.resolvePlugin = async (pluginName) => {
return Plugin1;
};
const loadFn = vi.fn();
class Plugin1 extends Plugin {
async afterAdd() {
loadFn();
}
async beforeLoad() {
loadFn();
}
async load() {
loadFn();
}
}
app = mockServer();
await app.cleanDb();
await app.load();
await app.install();
await app.pm.enable('Plugin1');
expect(loadFn).toBeCalledTimes(6);
await app.pm.enable('Plugin1');
expect(loadFn).toBeCalledTimes(6);
await app.pm.disable('Plugin1');
await app.pm.enable('Plugin1');
expect(loadFn).toBeCalledTimes(12);
PluginManager.resolvePlugin = resolvePlugin;
});
test('beforeEnable + install + afterEnable', async () => {
const resolvePlugin = PluginManager.resolvePlugin; const resolvePlugin = PluginManager.resolvePlugin;
PluginManager.resolvePlugin = async (pluginName) => { PluginManager.resolvePlugin = async (pluginName) => {
return Plugin1; return Plugin1;
@ -239,26 +272,72 @@ describe('pm', () => {
await app.cleanDb(); await app.cleanDb();
await app.load(); await app.load();
await app.install(); await app.install();
await app.pm.repository.create({
values: {
name: 'Plugin1',
},
});
await app.reload();
await app.pm.enable('Plugin1'); await app.pm.enable('Plugin1');
expect(loadFn).toBeCalledTimes(3);
expect(app.pm.get('Plugin1').enabled).toBeTruthy(); expect(app.pm.get('Plugin1').enabled).toBeTruthy();
expect(app.pm.get('Plugin1').installed).toBeTruthy(); expect(app.pm.get('Plugin1').installed).toBeTruthy();
expect(loadFn).toBeCalled();
expect(loadFn).toBeCalledTimes(3);
await app.pm.enable('Plugin1'); await app.pm.enable('Plugin1');
expect(loadFn).toBeCalledTimes(3); expect(loadFn).toBeCalledTimes(3);
expect(app.pm.get('Plugin1').enabled).toBeTruthy();
expect(app.pm.get('Plugin1').installed).toBeTruthy();
await app.pm.disable('Plugin1'); await app.pm.disable('Plugin1');
await app.pm.enable('Plugin1'); await app.pm.enable('Plugin1');
expect(loadFn).toBeCalledTimes(5); expect(loadFn).toBeCalledTimes(5);
PluginManager.resolvePlugin = resolvePlugin; PluginManager.resolvePlugin = resolvePlugin;
}); });
test('enable11', async () => {
test('afterAdd + beforeLoad + load', async () => {
const resolvePlugin = PluginManager.resolvePlugin;
class Plugin1 extends Plugin {
async afterAdd() {
loadFn();
}
async beforeLoad() {
loadFn();
}
async load() {
loadFn();
}
}
class Plugin2 extends Plugin {
async afterAdd() {
loadFn();
}
async beforeLoad() {
loadFn();
}
async load() {
loadFn();
}
}
PluginManager.resolvePlugin = async (pluginName: string) => {
return {
Plugin1,
Plugin2,
}[pluginName];
};
const loadFn = vi.fn(); const loadFn = vi.fn();
app = mockServer();
await app.cleanDb();
await app.load();
await app.install();
await app.pm.enable(['Plugin1', 'Plugin2']);
expect(loadFn).toBeCalledTimes(12);
await app.pm.enable('Plugin1');
expect(loadFn).toBeCalledTimes(12);
await app.pm.disable('Plugin1');
await app.pm.enable('Plugin1');
expect(loadFn).toBeCalledTimes(24);
PluginManager.resolvePlugin = resolvePlugin;
});
test('beforeEnable + install + afterEnable', async () => {
const resolvePlugin = PluginManager.resolvePlugin;
class Plugin1 extends Plugin { class Plugin1 extends Plugin {
async beforeEnable() { async beforeEnable() {
loadFn(); loadFn();
@ -270,6 +349,7 @@ describe('pm', () => {
loadFn(); loadFn();
} }
} }
class Plugin2 extends Plugin { class Plugin2 extends Plugin {
async beforeEnable() { async beforeEnable() {
loadFn(); loadFn();
@ -281,39 +361,33 @@ describe('pm', () => {
loadFn(); loadFn();
} }
} }
const resolvePlugin = PluginManager.resolvePlugin;
PluginManager.resolvePlugin = async (pluginName: string) => { PluginManager.resolvePlugin = async (pluginName: string) => {
return { return {
Plugin1, Plugin1,
Plugin2, Plugin2,
}[pluginName]; }[pluginName];
}; };
const loadFn = vi.fn();
app = mockServer(); app = mockServer();
await app.cleanDb(); await app.cleanDb();
await app.load(); await app.load();
await app.install(); await app.install();
await app.pm.repository.create({
values: [
{
name: 'Plugin1',
},
{
name: 'Plugin2',
},
],
});
await app.reload();
await app.pm.enable(['Plugin1', 'Plugin2']); await app.pm.enable(['Plugin1', 'Plugin2']);
expect(loadFn).toBeCalledTimes(6);
expect(app.pm.get('Plugin1').enabled).toBeTruthy(); expect(app.pm.get('Plugin1').enabled).toBeTruthy();
expect(app.pm.get('Plugin1').installed).toBeTruthy(); expect(app.pm.get('Plugin1').installed).toBeTruthy();
expect(app.pm.get('Plugin2').enabled).toBeTruthy();
expect(app.pm.get('Plugin2').installed).toBeTruthy();
expect(loadFn).toBeCalled();
expect(loadFn).toBeCalledTimes(6);
await app.pm.enable(['Plugin1', 'Plugin2']); await app.pm.enable(['Plugin1', 'Plugin2']);
expect(loadFn).toBeCalledTimes(6); expect(loadFn).toBeCalledTimes(6);
expect(app.pm.get('Plugin1').enabled).toBeTruthy();
expect(app.pm.get('Plugin1').installed).toBeTruthy();
await app.pm.disable(['Plugin1', 'Plugin2']);
await app.pm.enable(['Plugin1', 'Plugin2']);
expect(loadFn).toBeCalledTimes(10);
PluginManager.resolvePlugin = resolvePlugin; PluginManager.resolvePlugin = resolvePlugin;
}); });
test('disable', async () => { test('disable', async () => {
const resolvePlugin = PluginManager.resolvePlugin; const resolvePlugin = PluginManager.resolvePlugin;
PluginManager.resolvePlugin = async (pluginName) => { PluginManager.resolvePlugin = async (pluginName) => {
@ -338,10 +412,9 @@ describe('pm', () => {
}, },
}); });
await app.reload(); await app.reload();
expect(app.pm.get('Plugin1').enabled).toBeFalsy(); expect(app.pm.get('Plugin1')).toBeUndefined();
expect(app.pm.get('Plugin1').installed).toBeFalsy();
await app.pm.disable('Plugin1');
expect(loadFn).not.toBeCalled(); expect(loadFn).not.toBeCalled();
await expect(() => app.pm.disable('Plugin1')).rejects.toThrow('Plugin1 plugin does not exist');
PluginManager.resolvePlugin = resolvePlugin; PluginManager.resolvePlugin = resolvePlugin;
}); });
test('disable', async () => { test('disable', async () => {
@ -375,8 +448,13 @@ describe('pm', () => {
await app.pm.disable('Plugin1'); await app.pm.disable('Plugin1');
expect(loadFn).toBeCalled(); expect(loadFn).toBeCalled();
expect(loadFn).toBeCalledTimes(2); expect(loadFn).toBeCalledTimes(2);
expect(app.pm.get('Plugin1').enabled).toBeFalsy(); const instance = await app.pm.repository.findOne({
expect(app.pm.get('Plugin1').installed).toBeTruthy(); filter: {
name: 'Plugin1',
},
});
expect(instance.enabled).toBeFalsy();
expect(instance.installed).toBeTruthy();
PluginManager.resolvePlugin = resolvePlugin; PluginManager.resolvePlugin = resolvePlugin;
}); });
test('install', async () => { test('install', async () => {
@ -464,22 +542,22 @@ describe('pm', () => {
await app.cleanDb(); await app.cleanDb();
await app.load(); await app.load();
await app.install(); await app.install();
const plugin = await app.pm.repository.create({ await app.pm.repository.create({
values: { values: {
name: 'Plugin1', name: 'Plugin1',
}, },
}); });
await app.reload(); await app.reload();
expect(app.pm.get('Plugin1')['prop']).toBeUndefined(); expect(app.pm.has('Plugin1')).toBeFalsy();
expect(result).toEqual([]); expect(result).toEqual([]);
await app.pm.enable('Plugin1'); await app.pm.enable('Plugin1');
expect(app.pm.get('Plugin1')['prop']).toBe('a'); expect(app.pm.get('Plugin1')['prop']).toBe('a');
// console.log(hooks.join('/')); // console.log(hooks.join('/'));
expect(result).toEqual([false, true, true]); expect(result).toEqual([true, true, true]);
await app.pm.disable('Plugin1'); await app.pm.disable('Plugin1');
// console.log(hooks.join('/')); // console.log(hooks.join('/'));
expect(app.pm.get('Plugin1')['prop']).toBeUndefined(); expect(app.pm.has('Plugin1')).toBeFalsy();
expect(result).toEqual([false, true, true, true, false]); expect(result).toEqual([true, true, true, true, true]);
// console.log(hooks.join('/')); // console.log(hooks.join('/'));
PluginManager.resolvePlugin = resolvePlugin; PluginManager.resolvePlugin = resolvePlugin;
}); });

View File

@ -9,8 +9,7 @@
/* istanbul ignore file -- @preserve */ /* istanbul ignore file -- @preserve */
import { fsExists } from '@nocobase/utils'; import fs from 'fs-extra';
import fs from 'fs';
import { resolve } from 'path'; import { resolve } from 'path';
import Application from '../application'; import Application from '../application';
import { ApplicationNotInstall } from '../errors/application-not-install'; import { ApplicationNotInstall } from '../errors/application-not-install';
@ -23,12 +22,16 @@ export default (app: Application) => {
.option('--quickstart') .option('--quickstart')
.action(async (...cliArgs) => { .action(async (...cliArgs) => {
const [options] = cliArgs; const [options] = cliArgs;
const file = resolve(process.cwd(), 'storage/app-upgrading'); const file = resolve(process.cwd(), 'storage/.upgrading');
const upgrading = await fsExists(file); const upgrading = await fs.exists(file);
if (upgrading) { if (upgrading) {
await app.upgrade(); if (!process.env.VITEST) {
if (await app.isInstalled()) {
await app.upgrade();
}
}
try { try {
await fs.promises.rm(file); await fs.rm(file, { recursive: true, force: true });
} catch (error) { } catch (error) {
// skip // skip
} }

View File

@ -123,7 +123,9 @@ export default {
async list(ctx, next) { async list(ctx, next) {
const locale = ctx.getCurrentLocale(); const locale = ctx.getCurrentLocale();
const pm = ctx.app.pm as PluginManager; const pm = ctx.app.pm as PluginManager;
ctx.body = await pm.list({ locale, isPreset: false }); // ctx.body = await pm.list({ locale, isPreset: false });
const plugin = pm.get('nocobase') as any;
ctx.body = await plugin.getAllPlugins(locale);
await next(); await next();
}, },
async listEnabled(ctx, next) { async listEnabled(ctx, next) {
@ -156,7 +158,9 @@ export default {
if (!filterByTk) { if (!filterByTk) {
ctx.throw(400, 'plugin name invalid'); ctx.throw(400, 'plugin name invalid');
} }
ctx.body = await pm.get(filterByTk).toJSON({ locale }); const plugin = pm.get('nocobase') as any;
ctx.body = await plugin.getPluginInfo(filterByTk, locale);
// ctx.body = await pm.get(filterByTk).toJSON({ locale });
await next(); await next();
}, },
}, },

View File

@ -24,6 +24,8 @@ export class PluginManagerRepository extends Repository {
this.pm = pm; this.pm = pm;
} }
async createByName(nameOrPkgs) {}
async has(nameOrPkg: string) { async has(nameOrPkg: string) {
const { name } = await PluginManager.parseName(nameOrPkg); const { name } = await PluginManager.parseName(nameOrPkg);
const instance = await this.findOne({ const instance = await this.findOne({
@ -79,11 +81,19 @@ export class PluginManagerRepository extends Repository {
} }
async updateVersions() { async updateVersions() {
const items = await this.find(); const items = await this.find({
filter: {
enabled: true,
},
});
for (const item of items) { for (const item of items) {
const json = await PluginManager.getPackageJson(item.packageName); try {
item.set('version', json.version); const json = await PluginManager.getPackageJson(item.packageName);
await item.save(); item.set('version', json.version);
await item.save();
} catch (error) {
this.pm.app.log.error(error);
}
} }
} }
@ -117,6 +127,9 @@ export class PluginManagerRepository extends Repository {
} }
return await this.find({ return await this.find({
sort: 'id', sort: 'id',
filter: {
enabled: true,
},
}); });
} }

View File

@ -10,10 +10,9 @@
import Topo from '@hapi/topo'; import Topo from '@hapi/topo';
import { CleanOptions, Collection, SyncOptions } from '@nocobase/database'; import { CleanOptions, Collection, SyncOptions } from '@nocobase/database';
import { importModule, isURL } from '@nocobase/utils'; import { importModule, isURL } from '@nocobase/utils';
import { fsExists } from '@nocobase/utils/plugin-symlink';
import execa from 'execa'; import execa from 'execa';
import fg from 'fast-glob'; import fg from 'fast-glob';
import fs from 'fs'; import fs from 'fs-extra';
import _ from 'lodash'; import _ from 'lodash';
import net from 'net'; import net from 'net';
import { basename, join, resolve, sep } from 'path'; import { basename, join, resolve, sep } from 'path';
@ -26,12 +25,12 @@ import resourceOptions from './options/resource';
import { PluginManagerRepository } from './plugin-manager-repository'; import { PluginManagerRepository } from './plugin-manager-repository';
import { PluginData } from './types'; import { PluginData } from './types';
import { import {
checkAndGetCompatible,
copyTempPackageToStorageAndLinkToNodeModules, copyTempPackageToStorageAndLinkToNodeModules,
downloadAndUnzipToTempDir, downloadAndUnzipToTempDir,
getNpmInfo, getNpmInfo,
getPluginBasePath, getPluginBasePath,
getPluginInfoByNpm, getPluginInfoByNpm,
removeTmpDir,
updatePluginByCompressedFileUrl, updatePluginByCompressedFileUrl,
} from './utils'; } from './utils';
@ -56,6 +55,8 @@ export interface InstallOptions {
export class AddPresetError extends Error {} export class AddPresetError extends Error {}
export class PluginManager { export class PluginManager {
static checkAndGetCompatible = checkAndGetCompatible;
/** /**
* @internal * @internal
*/ */
@ -118,12 +119,19 @@ export class PluginManager {
return this.app.db.getRepository('applicationPlugins') as PluginManagerRepository; return this.app.db.getRepository('applicationPlugins') as PluginManagerRepository;
} }
static async packageExists(nameOrPkg: string) {
const { packageName } = await this.parseName(nameOrPkg);
const file = resolve(process.env.NODE_MODULES_PATH, packageName, 'package.json');
return fs.exists(file);
}
/** /**
* @internal * @internal
*/ */
static async getPackageJson(packageName: string) { static async getPackageJson(nameOrPkg: string) {
const file = await fs.promises.realpath(resolve(process.env.NODE_MODULES_PATH, packageName, 'package.json')); const { packageName } = await this.parseName(nameOrPkg);
const data = await fs.promises.readFile(file, { encoding: 'utf-8' }); const file = await fs.realpath(resolve(process.env.NODE_MODULES_PATH, packageName, 'package.json'));
const data = await fs.readFile(file, { encoding: 'utf-8' });
return JSON.parse(data); return JSON.parse(data);
} }
@ -134,7 +142,7 @@ export class PluginManager {
const prefixes = this.getPluginPkgPrefix(); const prefixes = this.getPluginPkgPrefix();
for (const prefix of prefixes) { for (const prefix of prefixes) {
const pkg = resolve(process.env.NODE_MODULES_PATH, `${prefix}${name}`, 'package.json'); const pkg = resolve(process.env.NODE_MODULES_PATH, `${prefix}${name}`, 'package.json');
const exists = await fsExists(pkg); const exists = await fs.exists(pkg);
if (exists) { if (exists) {
return `${prefix}${name}`; return `${prefix}${name}`;
} }
@ -196,9 +204,7 @@ export class PluginManager {
*/ */
static async resolvePlugin(pluginName: string | typeof Plugin, isUpgrade = false, isPkg = false) { static async resolvePlugin(pluginName: string | typeof Plugin, isUpgrade = false, isPkg = false) {
if (typeof pluginName === 'string') { if (typeof pluginName === 'string') {
const packageName = isPkg ? pluginName : await this.getPackageName(pluginName); const { packageName } = await this.parseName(pluginName);
this.clearCache(packageName);
return await importModule(packageName); return await importModule(packageName);
} else { } else {
return pluginName; return pluginName;
@ -226,7 +232,7 @@ export class PluginManager {
return this.parsedNames[nameOrPkg]; return this.parsedNames[nameOrPkg];
} }
const exists = async (name: string, isPreset = false) => { const exists = async (name: string, isPreset = false) => {
return fsExists( return fs.exists(
resolve(process.env.NODE_MODULES_PATH, `@nocobase/${isPreset ? 'preset' : 'plugin'}-${name}`, 'package.json'), resolve(process.env.NODE_MODULES_PATH, `@nocobase/${isPreset ? 'preset' : 'plugin'}-${name}`, 'package.json'),
); );
}; };
@ -285,7 +291,7 @@ export class PluginManager {
const createPlugin = async (name) => { const createPlugin = async (name) => {
const pluginDir = resolve(process.cwd(), 'packages/plugins', name); const pluginDir = resolve(process.cwd(), 'packages/plugins', name);
if (options?.forceRecreate) { if (options?.forceRecreate) {
await fs.promises.rm(pluginDir, { recursive: true, force: true }); await fs.rm(pluginDir, { recursive: true, force: true });
} }
const { PluginGenerator } = require('@nocobase/cli/src/plugin-generator'); const { PluginGenerator } = require('@nocobase/cli/src/plugin-generator');
const generator = new PluginGenerator({ const generator = new PluginGenerator({
@ -298,16 +304,6 @@ export class PluginManager {
await generator.run(); await generator.run();
}; };
await createPlugin(pluginName); await createPlugin(pluginName);
try {
await this.app.db.auth({ retry: 1 });
const installed = await this.app.isInstalled();
if (!installed) {
console.log(`yarn pm add ${pluginName}`);
return;
}
} catch (error) {
return;
}
this.app.log.info('attempt to add the plugin to the app'); this.app.log.info('attempt to add the plugin to the app');
const { name, packageName } = await PluginManager.parseName(pluginName); const { name, packageName } = await PluginManager.parseName(pluginName);
const json = await PluginManager.getPackageJson(packageName); const json = await PluginManager.getPackageJson(packageName);
@ -316,15 +312,6 @@ export class PluginManager {
packageName, packageName,
version: json.version, version: json.version,
}); });
await this.repository.updateOrCreate({
values: {
name,
packageName,
version: json.version,
},
filterKeys: ['name'],
});
await sleep(1000);
await tsxRerunning(); await tsxRerunning();
} }
@ -374,14 +361,6 @@ export class PluginManager {
if (options.packageName) { if (options.packageName) {
this.pluginAliases.set(options.packageName, instance); this.pluginAliases.set(options.packageName, instance);
} }
if (insert && options.name) {
await this.repository.updateOrCreate({
values: {
...options,
},
filterKeys: ['name'],
});
}
await instance.afterAdd(); await instance.afterAdd();
} }
@ -524,16 +503,69 @@ export class PluginManager {
async enable(nameOrPkg: string | string[]) { async enable(nameOrPkg: string | string[]) {
let pluginNames = nameOrPkg; let pluginNames = nameOrPkg;
if (nameOrPkg === '*') { if (nameOrPkg === '*') {
const items = await this.repository.find(); const plugin = this.get('nocobase') as any;
pluginNames = items.map((item: any) => item.name); pluginNames = await plugin.findLocalPlugins();
}
pluginNames = await this.sort(pluginNames);
try {
const added = {};
for (const name of pluginNames) {
const { name: pluginName } = await PluginManager.parseName(name);
if (this.has(pluginName)) {
added[pluginName] = true;
continue;
}
await this.add(pluginName);
}
for (const name of pluginNames) {
const { name: pluginName } = await PluginManager.parseName(name);
const plugin = this.get(pluginName);
if (!plugin) {
throw new Error(`${pluginName} plugin does not exist`);
}
if (added[pluginName]) {
continue;
}
const instance = await this.repository.findOne({
filter: {
name: pluginName,
},
});
if (instance) {
plugin.enabled = instance.enabled;
plugin.installed = instance.installed;
}
if (plugin.enabled) {
continue;
}
await plugin.beforeLoad();
}
for (const name of pluginNames) {
const { name: pluginName } = await PluginManager.parseName(name);
const plugin = this.get(pluginName);
if (!plugin) {
throw new Error(`${pluginName} plugin does not exist`);
}
if (added[pluginName]) {
continue;
}
if (plugin.enabled) {
continue;
}
await plugin.loadCollections();
await plugin.load();
}
} catch (error) {
await this.app.tryReloadOrRestart({
recover: true,
});
throw error;
} }
pluginNames = this.sort(pluginNames);
this.app.log.debug(`enabling plugin ${pluginNames.join(',')}`); this.app.log.debug(`enabling plugin ${pluginNames.join(',')}`);
this.app.setMaintainingMessage(`enabling plugin ${pluginNames.join(',')}`); this.app.setMaintainingMessage(`enabling plugin ${pluginNames.join(',')}`);
const toBeUpdated = []; const toBeUpdated = [];
for (const name of pluginNames) { for (const name of pluginNames) {
const { name: pluginName } = await PluginManager.parseName(name); const { name: pluginName } = await PluginManager.parseName(name);
console.log('pluginName', pluginName);
const plugin = this.get(pluginName); const plugin = this.get(pluginName);
if (!plugin) { if (!plugin) {
throw new Error(`${pluginName} plugin does not exist`); throw new Error(`${pluginName} plugin does not exist`);
@ -544,7 +576,6 @@ export class PluginManager {
await this.app.emitAsync('beforeEnablePlugin', pluginName); await this.app.emitAsync('beforeEnablePlugin', pluginName);
try { try {
await plugin.beforeEnable(); await plugin.beforeEnable();
plugin.enabled = true;
toBeUpdated.push(pluginName); toBeUpdated.push(pluginName);
} catch (error) { } catch (error) {
if (nameOrPkg === '*') { if (nameOrPkg === '*') {
@ -557,16 +588,7 @@ export class PluginManager {
if (toBeUpdated.length === 0) { if (toBeUpdated.length === 0) {
return; return;
} }
await this.repository.update({
filter: {
name: toBeUpdated,
},
values: {
enabled: true,
},
});
try { try {
await this.app.reload();
this.app.log.debug(`syncing database in enable plugin ${toBeUpdated.join(',')}...`); this.app.log.debug(`syncing database in enable plugin ${toBeUpdated.join(',')}...`);
this.app.setMaintainingMessage(`syncing database in enable plugin ${toBeUpdated.join(',')}...`); this.app.setMaintainingMessage(`syncing database in enable plugin ${toBeUpdated.join(',')}...`);
await this.app.db.sync(); await this.app.db.sync();
@ -579,32 +601,31 @@ export class PluginManager {
plugin.installed = true; plugin.installed = true;
} }
} }
await this.repository.update({ for (const pluginName of toBeUpdated) {
filter: { const { name } = await PluginManager.parseName(pluginName);
name: toBeUpdated, const packageJson = await PluginManager.getPackageJson(pluginName);
}, const values = {
values: { name,
packageName: packageJson?.name,
enabled: true,
installed: true, installed: true,
}, version: packageJson?.version,
}); };
await this.repository.updateOrCreate({
values,
filterKeys: ['name'],
});
}
for (const pluginName of toBeUpdated) { for (const pluginName of toBeUpdated) {
const plugin = this.get(pluginName); const plugin = this.get(pluginName);
this.app.log.debug(`emit afterEnablePlugin event...`); this.app.log.debug(`emit afterEnablePlugin event...`);
await plugin.afterEnable(); await plugin.afterEnable();
plugin.enabled = true;
await this.app.emitAsync('afterEnablePlugin', pluginName); await this.app.emitAsync('afterEnablePlugin', pluginName);
this.app.log.debug(`afterEnablePlugin event emitted`); this.app.log.debug(`afterEnablePlugin event emitted`);
} }
await this.app.tryReloadOrRestart(); await this.app.tryReloadOrRestart();
} catch (error) { } catch (error) {
await this.repository.update({
filter: {
name: toBeUpdated,
},
values: {
enabled: false,
installed: false,
},
});
await this.app.tryReloadOrRestart({ await this.app.tryReloadOrRestart({
recover: true, recover: true,
}); });
@ -634,32 +655,24 @@ export class PluginManager {
if (toBeUpdated.length === 0) { if (toBeUpdated.length === 0) {
return; return;
} }
await this.repository.update({
filter: {
name: toBeUpdated,
},
values: {
enabled: false,
},
});
try { try {
await this.app.tryReloadOrRestart(); for (const pluginName of toBeUpdated) {
for (const pluginName of pluginNames) {
const plugin = this.get(pluginName); const plugin = this.get(pluginName);
this.app.log.debug(`emit afterDisablePlugin event...`); this.app.log.debug(`emit afterDisablePlugin event...`);
await plugin.afterDisable(); await plugin.afterDisable();
await this.app.emitAsync('afterDisablePlugin', pluginName); await this.app.emitAsync('afterDisablePlugin', pluginName);
this.app.log.debug(`afterDisablePlugin event emitted`); this.app.log.debug(`afterDisablePlugin event emitted`);
} }
} catch (error) {
await this.repository.update({ await this.repository.update({
filter: { filter: {
name: toBeUpdated, name: toBeUpdated,
}, },
values: { values: {
enabled: true, enabled: false,
}, },
}); });
await this.app.tryReloadOrRestart();
} catch (error) {
await this.app.tryReloadOrRestart({ await this.app.tryReloadOrRestart({
recover: true, recover: true,
}); });
@ -684,72 +697,43 @@ export class PluginManager {
records.map(async (plugin) => { records.map(async (plugin) => {
const dir = resolve(process.env.NODE_MODULES_PATH, plugin.packageName); const dir = resolve(process.env.NODE_MODULES_PATH, plugin.packageName);
try { try {
const realDir = await fs.promises.realpath(dir); const realDir = await fs.realpath(dir);
console.log('realDir', realDir);
this.app.log.debug(`rm -rf ${realDir}`); this.app.log.debug(`rm -rf ${realDir}`);
return fs.promises.rm(realDir, { force: true, recursive: true }); return fs.rm(realDir, { force: true, recursive: true });
} catch (error) { } catch (error) {
return false; return false;
} }
}), }),
); );
await execa('yarn', ['nocobase', 'postinstall']);
}; };
if (options?.force) { await this.repository.destroy({
await this.repository.destroy({ filter: {
filter: { name: pluginNames,
name: pluginNames, },
}, });
}); if (!this.app.db.getCollection('applications')) {
this.app.log.warn(`force remove plugins ${pluginNames.join(',')}`);
} else {
await this.app.load();
for (const pluginName of pluginNames) {
const plugin = this.get(pluginName);
if (!plugin) {
continue;
}
if (plugin.enabled) {
throw new Error(`plugin is enabled [${pluginName}]`);
}
await plugin.beforeRemove();
}
await this.repository.destroy({
filter: {
name: pluginNames,
},
});
const plugins: Plugin[] = [];
for (const pluginName of pluginNames) {
const plugin = this.get(pluginName);
if (!plugin) {
continue;
}
plugins.push(plugin);
this.del(pluginName);
await plugin.afterRemove();
}
if (await this.app.isStarted()) {
await this.app.tryReloadOrRestart();
}
}
if (options?.removeDir) {
await removeDir(); await removeDir();
} }
await execa('yarn', ['nocobase', 'refresh'], {
env: process.env,
});
} }
/** /**
* @internal * @internal
*/ */
async addViaCLI(urlOrName: string | string[], options?: PluginData, emitStartedEvent = true) { async addViaCLI(urlOrName: string | string[], options?: PluginData, emitStartedEvent = true) {
const writeFile = async () => {
if (process.env.VITEST) {
return;
}
const file = resolve(process.cwd(), 'storage/.upgrading');
this.app.log.debug('pending upgrade');
await fs.writeFile(file, 'upgrading');
};
await writeFile();
if (Array.isArray(urlOrName)) { if (Array.isArray(urlOrName)) {
for (const packageName of urlOrName) { for (const packageName of urlOrName) {
await this.addViaCLI(packageName, _.omit(options, 'name'), false); await this.addViaCLI(packageName, _.omit(options, 'name'), false);
} }
await this.app.emitStartedEvent();
await execa('yarn', ['nocobase', 'postinstall']);
return; return;
} }
if (isURL(urlOrName)) { if (isURL(urlOrName)) {
@ -760,7 +744,7 @@ export class PluginManager {
}, },
emitStartedEvent, emitStartedEvent,
); );
} else if (await fsExists(urlOrName)) { } else if (await fs.exists(urlOrName)) {
await this.addByCompressedFileUrl( await this.addByCompressedFileUrl(
{ {
...(options as any), ...(options as any),
@ -778,20 +762,6 @@ export class PluginManager {
}, },
emitStartedEvent, emitStartedEvent,
); );
} else {
const { name, packageName } = await PluginManager.parseName(urlOrName);
const opts = {
...options,
name,
packageName,
};
// 下面这行代码删了,测试会报错 packages/core/server/src/__tests__/gateway.test.ts:407:29
await this.repository.findOne({ filter: { packageName } });
await this.add(name, opts, true);
}
if (emitStartedEvent) {
await this.app.emitStartedEvent();
await execa('yarn', ['nocobase', 'postinstall']);
} }
} }
@ -826,19 +796,7 @@ export class PluginManager {
const { packageName, tempFile, tempPackageContentDir } = await downloadAndUnzipToTempDir(file, authToken); const { packageName, tempFile, tempPackageContentDir } = await downloadAndUnzipToTempDir(file, authToken);
const { name } = await PluginManager.parseName(packageName);
if (this.has(name)) {
await removeTmpDir(tempFile, tempPackageContentDir);
if (throwError) {
throw new Error(`plugin name [${name}] already exists`);
} else {
this.app.log.warn(`plugin name [${name}] already exists`);
return;
}
}
await copyTempPackageToStorageAndLinkToNodeModules(tempFile, tempPackageContentDir, packageName); await copyTempPackageToStorageAndLinkToNodeModules(tempFile, tempPackageContentDir, packageName);
return this.add(name, { packageName }, true);
} }
/** /**
@ -861,19 +819,7 @@ export class PluginManager {
authToken, authToken,
); );
const { name } = await PluginManager.parseName(packageName);
if (this.has(name)) {
await removeTmpDir(tempFile, tempPackageContentDir);
if (throwError) {
throw new Error(`plugin name [${name}] already exists`);
} else {
this.app.log.warn(`plugin name [${name}] already exists`);
return;
}
}
await copyTempPackageToStorageAndLinkToNodeModules(tempFile, tempPackageContentDir, packageName); await copyTempPackageToStorageAndLinkToNodeModules(tempFile, tempPackageContentDir, packageName);
return this.add(name, { packageName }, true);
} }
async update(nameOrPkg: string | string[], options: PluginData, emitStartedEvent = true) { async update(nameOrPkg: string | string[], options: PluginData, emitStartedEvent = true) {
@ -888,7 +834,7 @@ export class PluginManager {
return; return;
} }
const file = resolve(process.cwd(), 'storage/app-upgrading'); const file = resolve(process.cwd(), 'storage/app-upgrading');
await fs.promises.writeFile(file, '', 'utf-8'); await fs.writeFile(file, '', 'utf-8');
// await this.app.upgrade(); // await this.app.upgrade();
await tsxRerunning(); await tsxRerunning();
await execa('yarn', ['nocobase', 'pm2-restart'], { await execa('yarn', ['nocobase', 'pm2-restart'], {
@ -904,7 +850,7 @@ export class PluginManager {
const opts = { ...options }; const opts = { ...options };
if (isURL(nameOrPkg)) { if (isURL(nameOrPkg)) {
opts.compressedFileUrl = nameOrPkg; opts.compressedFileUrl = nameOrPkg;
} else if (await fsExists(nameOrPkg)) { } else if (await fs.exists(nameOrPkg)) {
opts.compressedFileUrl = nameOrPkg; opts.compressedFileUrl = nameOrPkg;
} }
if (opts.compressedFileUrl) { if (opts.compressedFileUrl) {
@ -958,7 +904,7 @@ export class PluginManager {
repository: this.repository, repository: this.repository,
}); });
const { name } = await PluginManager.parseName(packageName); const { name } = await PluginManager.parseName(packageName);
await this.add(name, { name, version, packageName }, true, true); // await this.add(name, { name, version, packageName }, true, true);
} }
/** /**
@ -1152,16 +1098,16 @@ export class PluginManager {
this['_initPresetPlugins'] = true; this['_initPresetPlugins'] = true;
} }
private sort(names: string | string[]) { private async sort(names: string | string[]) {
const pluginNames = _.castArray(names); const pluginNames = _.castArray(names);
if (pluginNames.length === 1) { if (pluginNames.length === 1) {
return pluginNames; return pluginNames;
} }
const sorter = new Topo.Sorter<string>(); const sorter = new Topo.Sorter<string>();
for (const pluginName of pluginNames) { for (const pluginName of pluginNames) {
const plugin = this.get(pluginName); const packageJson = await PluginManager.getPackageJson(pluginName);
const peerDependencies = Object.keys(plugin.options?.packageJson?.peerDependencies || {}); const peerDependencies = Object.keys(packageJson?.peerDependencies || {});
sorter.add(pluginName, { after: peerDependencies, group: plugin.options?.packageName || pluginName }); sorter.add(pluginName, { after: peerDependencies, group: packageJson?.packageName || pluginName });
} }
return sorter.nodes; return sorter.nodes;
} }

View File

@ -255,12 +255,12 @@ describe.runIf(isPg())('collection sync', () => {
}); });
}; };
expect((await getSubAppMapRecord(sub1)).get('enabled')).toBeFalsy(); expect(await getSubAppMapRecord(sub1)).toBeNull();
await mainApp.pm.enable(['map']); await mainApp.pm.enable(['map']);
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
expect((await getSubAppMapRecord(sub1)).get('enabled')).toBeTruthy(); expect((await getSubAppMapRecord(sub1)).enabled).toBeTruthy();
// create new app sub2 // create new app sub2
await mainApp.db.getRepository('applications').create({ await mainApp.db.getRepository('applications').create({
values: { values: {

View File

@ -127,7 +127,7 @@ export class MultiAppShareCollectionPlugin extends Plugin {
throw new Error('multi-app-share-collection plugin only support postgres'); throw new Error('multi-app-share-collection plugin only support postgres');
} }
const plugin = this.pm.get('multi-app-manager'); const plugin = this.pm.get('multi-app-manager');
if (!plugin.enabled) { if (!plugin?.enabled) {
throw new Error(`${this.name} plugin need multi-app-manager plugin enabled`); throw new Error(`${this.name} plugin need multi-app-manager plugin enabled`);
} }
} }

View File

@ -66,12 +66,62 @@
"@nocobase/plugin-workflow-request": "1.4.0-alpha", "@nocobase/plugin-workflow-request": "1.4.0-alpha",
"@nocobase/plugin-workflow-sql": "1.4.0-alpha", "@nocobase/plugin-workflow-sql": "1.4.0-alpha",
"@nocobase/server": "1.4.0-alpha", "@nocobase/server": "1.4.0-alpha",
"cronstrue": "^2.11.0" "cronstrue": "^2.11.0",
"fs-extra": "^11.1.1"
}, },
"deprecated": [
"@nocobase/plugin-audit-logs",
"@nocobase/plugin-charts",
"@nocobase/plugin-mobile-client",
"@nocobase/plugin-snapshot-field"
],
"builtIn": [
"@nocobase/plugin-acl",
"@nocobase/plugin-action-bulk-edit",
"@nocobase/plugin-action-bulk-update",
"@nocobase/plugin-action-custom-request",
"@nocobase/plugin-action-duplicate",
"@nocobase/plugin-action-export",
"@nocobase/plugin-action-import",
"@nocobase/plugin-action-print",
"@nocobase/plugin-auth",
"@nocobase/plugin-block-iframe",
"@nocobase/plugin-block-workbench",
"@nocobase/plugin-calendar",
"@nocobase/plugin-client",
"@nocobase/plugin-collection-sql",
"@nocobase/plugin-collection-tree",
"@nocobase/plugin-data-source-main",
"@nocobase/plugin-data-source-manager",
"@nocobase/plugin-data-visualization",
"@nocobase/plugin-error-handler",
"@nocobase/plugin-field-china-region",
"@nocobase/plugin-field-formula",
"@nocobase/plugin-field-sequence",
"@nocobase/plugin-file-manager",
"@nocobase/plugin-gantt",
"@nocobase/plugin-kanban",
"@nocobase/plugin-logger",
"@nocobase/plugin-system-settings",
"@nocobase/plugin-ui-schema-storage",
"@nocobase/plugin-user-data-sync",
"@nocobase/plugin-users",
"@nocobase/plugin-verification",
"@nocobase/plugin-workflow",
"@nocobase/plugin-workflow-action-trigger",
"@nocobase/plugin-workflow-aggregate",
"@nocobase/plugin-workflow-delay",
"@nocobase/plugin-workflow-dynamic-calculation",
"@nocobase/plugin-workflow-loop",
"@nocobase/plugin-workflow-manual",
"@nocobase/plugin-workflow-parallel",
"@nocobase/plugin-workflow-request",
"@nocobase/plugin-workflow-sql"
],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/nocobase/nocobase.git", "url": "git+https://github.com/nocobase/nocobase.git",
"directory": "packages/presets/nocobase" "directory": "packages/presets/nocobase"
}, },
"gitHead": "d0b4efe4be55f8c79a98a331d99d9f8cf99021a1" "gitHead": "d0b4efe4be55f8c79a98a331d99d9f8cf99021a1"
} }

View File

@ -0,0 +1,114 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { PluginManager } from '@nocobase/server';
import fg from 'fast-glob';
import fs from 'fs-extra';
import _ from 'lodash';
import path from 'path';
function splitNames(name: string) {
return (name || '').split(',').filter(Boolean);
}
export async function trim(packageNames: string[]) {
const nameOrPkgs = _.uniq(packageNames).filter(Boolean);
const names = [];
for (const nameOrPkg of nameOrPkgs) {
const { name, packageName } = await PluginManager.parseName(nameOrPkg);
try {
await PluginManager.getPackageJson(packageName);
names.push(name);
} catch (error) {
//
}
}
return names;
}
export async function findPackageNames() {
const patterns = [
'./packages/plugins/*/package.json',
'./packages/plugins/*/*/package.json',
'./packages/pro-plugins/*/*/package.json',
'./storage/plugins/*/package.json',
'./storage/plugins/*/*/package.json',
];
try {
const packageJsonPaths = await fg(patterns, {
cwd: process.cwd(),
absolute: true,
ignore: ['**/external-db-data-source/**'],
});
const packageNames = await Promise.all(
packageJsonPaths.map(async (packageJsonPath) => {
const packageJson = await fs.readJson(packageJsonPath);
return packageJson.name;
}),
);
const excludes = [
'@nocobase/plugin-audit-logs',
'@nocobase/plugin-backup-restore',
'@nocobase/plugin-charts',
'@nocobase/plugin-disable-pm-add',
'@nocobase/plugin-mobile-client',
'@nocobase/plugin-mock-collections',
'@nocobase/plugin-multi-app-share-collection',
'@nocobase/plugin-notifications',
'@nocobase/plugin-snapshot-field',
'@nocobase/plugin-workflow-test',
];
const nocobasePlugins = await findNocobasePlugins();
const { APPEND_PRESET_BUILT_IN_PLUGINS = '', APPEND_PRESET_LOCAL_PLUGINS = '' } = process.env;
return trim(
_.difference(packageNames, excludes)
.filter(Boolean)
.concat(nocobasePlugins)
.concat(splitNames(APPEND_PRESET_BUILT_IN_PLUGINS))
.concat(splitNames(APPEND_PRESET_LOCAL_PLUGINS)),
);
} catch (error) {
return [];
}
}
async function findNocobasePlugins() {
try {
const packageJson = await fs.readJson(path.resolve(__dirname, '../../package.json'));
const pluginNames = Object.keys(packageJson.dependencies).filter((name) => name.startsWith('@nocobase/plugin-'));
return trim(pluginNames);
} catch (error) {
return [];
}
}
export async function findBuiltInPlugins() {
const { APPEND_PRESET_BUILT_IN_PLUGINS = '' } = process.env;
try {
const packageJson = await fs.readJson(path.resolve(__dirname, '../../package.json'));
return trim(packageJson.builtIn.concat(splitNames(APPEND_PRESET_BUILT_IN_PLUGINS)));
} catch (error) {
return [];
}
}
export async function findLocalPlugins() {
const { APPEND_PRESET_LOCAL_PLUGINS = '' } = process.env;
const plugins1 = await findNocobasePlugins();
const plugins2 = await findPackageNames();
const builtInPlugins = await findBuiltInPlugins();
const packageJson = await fs.readJson(path.resolve(__dirname, '../../package.json'));
const items = await trim(
_.difference(
plugins1.concat(plugins2).concat(splitNames(APPEND_PRESET_LOCAL_PLUGINS)),
builtInPlugins.concat(await trim(packageJson.deprecated)),
),
);
return items;
}

View File

@ -9,88 +9,78 @@
import { Plugin, PluginManager } from '@nocobase/server'; import { Plugin, PluginManager } from '@nocobase/server';
import _ from 'lodash'; import _ from 'lodash';
import { findBuiltInPlugins, findLocalPlugins, trim } from './findPackageNames';
export class PresetNocoBase extends Plugin { export class PresetNocoBase extends Plugin {
builtInPlugins = [
'data-source-manager',
'error-handler',
'data-source-main',
'ui-schema-storage',
// 'ui-routes-storage',
'file-manager',
'system-settings',
'field-sequence',
'verification',
'users',
'user-data-sync',
'acl',
'field-china-region',
'workflow',
'workflow-action-trigger',
'workflow-aggregate',
'workflow-delay',
'workflow-dynamic-calculation',
'workflow-loop',
'workflow-manual',
'workflow-parallel',
'workflow-request',
'workflow-sql',
'client',
'action-import',
'action-export',
'block-iframe',
'block-workbench',
'field-formula',
'data-visualization',
'auth',
'logger',
'action-custom-request',
'calendar',
'action-bulk-update',
'action-bulk-edit',
'gantt',
'kanban',
'action-duplicate',
'action-print',
'collection-sql',
'collection-tree',
];
localPlugins = [
'multi-app-manager>=0.7.0-alpha.1',
// 'audit-logs>=0.7.1-alpha.4',
'map>=0.8.1-alpha.3',
// 'snapshot-field>=0.8.1-alpha.3',
'graph-collection-manager>=0.9.0-alpha.1',
// 'multi-app-share-collection>=0.9.2-alpha.1',
'mobile',
// 'mobile-client>=0.10.0-alpha.2',
'api-keys>=0.10.1-alpha.1',
'localization>=0.11.1-alpha.1',
'theme-editor>=0.11.1-alpha.1',
'api-doc>=0.13.0-alpha.1',
'auth-sms>=0.10.0-alpha.2',
'field-markdown-vditor>=0.21.0-alpha.16',
'workflow-mailer',
'field-m2m-array',
'backup-restore',
];
splitNames(name: string) { splitNames(name: string) {
return (name || '').split(',').filter(Boolean); return (name || '').split(',').filter(Boolean);
} }
getBuiltInPlugins() { async getBuiltInPlugins() {
const { APPEND_PRESET_BUILT_IN_PLUGINS } = process.env; return await findBuiltInPlugins();
return _.uniq(this.splitNames(APPEND_PRESET_BUILT_IN_PLUGINS).concat(this.builtInPlugins));
} }
getLocalPlugins() { async getLocalPlugins() {
const { APPEND_PRESET_LOCAL_PLUGINS } = process.env; return [];
const plugins = this.splitNames(APPEND_PRESET_LOCAL_PLUGINS) return (await findLocalPlugins()).map((name) => name.split('>='));
.concat(this.localPlugins) }
.map((name) => name.split('>='));
return plugins; async findLocalPlugins() {
return await findLocalPlugins();
}
async getAllPluginNames() {
const plugins1 = await findBuiltInPlugins();
const plugins2 = await findLocalPlugins();
return [...plugins1, ...plugins2];
}
async getAllPluginNamesAndDB() {
const items = await this.pm.repository.find({
filter: {
enabled: true,
},
});
const plugins1 = await findBuiltInPlugins();
const plugins2 = await findLocalPlugins();
return trim(_.uniq([...plugins1, ...plugins2, ...items.map((item) => item.name)]));
}
async getAllPlugins(locale = 'en-US') {
const plugins = await this.getAllPluginNamesAndDB();
const packageJsons = [];
for (const name of plugins) {
packageJsons.push(await this.getPluginInfo(name, locale));
}
return packageJsons;
}
async getPluginInfo(name, locale = 'en-US') {
const repository = this.app.db.getRepository<any>('applicationPlugins');
// const packageJson = await this.getPackageJson(name);
const { packageName } = await PluginManager.parseName(name);
const packageJson = require(`${packageName}/package.json`);
const deps = await PluginManager.checkAndGetCompatible(packageJson.name);
const instance = await repository.findOne({
filter: {
packageName: packageJson.name,
},
});
return {
packageName: packageJson.name,
name: name,
version: packageJson.version,
enabled: !!instance?.enabled,
installed: !!instance?.installed,
builtIn: !!instance?.builtIn,
keywords: packageJson.keywords,
author: packageJson.author,
packageJson,
removable: !instance?.enabled && !this.app.db.hasCollection('applications'),
displayName: packageJson?.[`displayName.${locale}`] || packageJson?.displayName || name,
description: packageJson?.[`description.${locale}`] || packageJson.description,
...deps,
};
} }
async getPackageJson(name) { async getPackageJson(name) {
@ -100,9 +90,11 @@ export class PresetNocoBase extends Plugin {
} }
async allPlugins() { async allPlugins() {
const builtInPlugins = await this.getBuiltInPlugins();
const localPlugins = await this.getLocalPlugins();
return ( return (
await Promise.all( await Promise.all(
this.getBuiltInPlugins().map(async (pkgOrName) => { builtInPlugins.map(async (pkgOrName) => {
const { name } = await PluginManager.parseName(pkgOrName); const { name } = await PluginManager.parseName(pkgOrName);
const packageJson = await this.getPackageJson(pkgOrName); const packageJson = await this.getPackageJson(pkgOrName);
return { return {
@ -116,7 +108,7 @@ export class PresetNocoBase extends Plugin {
) )
).concat( ).concat(
await Promise.all( await Promise.all(
this.getLocalPlugins().map(async (plugin) => { localPlugins.map(async (plugin) => {
const { name } = await PluginManager.parseName(plugin[0]); const { name } = await PluginManager.parseName(plugin[0]);
const packageJson = await this.getPackageJson(plugin[0]); const packageJson = await this.getPackageJson(plugin[0]);
return { name, packageName: packageJson.name, version: packageJson.version }; return { name, packageName: packageJson.name, version: packageJson.version };
@ -128,8 +120,10 @@ export class PresetNocoBase extends Plugin {
async getPluginToBeUpgraded() { async getPluginToBeUpgraded() {
const repository = this.app.db.getRepository<any>('applicationPlugins'); const repository = this.app.db.getRepository<any>('applicationPlugins');
const items = (await repository.find()).map((item) => item.name); const items = (await repository.find()).map((item) => item.name);
const builtInPlugins = await this.getBuiltInPlugins();
const localPlugins = await this.getLocalPlugins();
const plugins = await Promise.all( const plugins = await Promise.all(
this.getBuiltInPlugins().map(async (pkgOrName) => { builtInPlugins.map(async (pkgOrName) => {
const { name } = await PluginManager.parseName(pkgOrName); const { name } = await PluginManager.parseName(pkgOrName);
const packageJson = await this.getPackageJson(pkgOrName); const packageJson = await this.getPackageJson(pkgOrName);
return { return {
@ -141,7 +135,7 @@ export class PresetNocoBase extends Plugin {
} as any; } as any;
}), }),
); );
for (const plugin of this.getLocalPlugins()) { for (const plugin of localPlugins) {
if (plugin[1]) { if (plugin[1]) {
// 不在插件列表,并且插件最低版本小于当前应用版本,跳过不处理 // 不在插件列表,并且插件最低版本小于当前应用版本,跳过不处理
if (!items.includes(plugin[0]) && (await this.app.version.satisfies(`>${plugin[1]}`))) { if (!items.includes(plugin[0]) && (await this.app.version.satisfies(`>${plugin[1]}`))) {