mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 05:29:26 +08:00
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:
parent
475be58aa7
commit
d7dc8fa4cf
1
.gitignore
vendored
1
.gitignore
vendored
@ -32,6 +32,7 @@ storage/plugins
|
||||
storage/tar
|
||||
storage/tmp
|
||||
storage/app.watch.ts
|
||||
storage/.upgrading
|
||||
storage/logs-e2e
|
||||
storage/uploads-e2e
|
||||
storage/.pm2-*
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getUmiConfig, IndexGenerator } from '@nocobase/devtools/umiConfig';
|
||||
import { generatePlugins, getUmiConfig } from '@nocobase/devtools/umiConfig';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'umi';
|
||||
|
||||
@ -8,17 +8,11 @@ process.env.MFSU_AD = 'none';
|
||||
process.env.DID_YOU_KNOW = 'none';
|
||||
|
||||
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 appPublicPath = isDevCmd ? '/' : '{{env.APP_PUBLIC_PATH}}';
|
||||
|
||||
generatePlugins();
|
||||
|
||||
export default defineConfig({
|
||||
title: 'Loading...',
|
||||
devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false,
|
||||
|
@ -9,8 +9,12 @@
|
||||
|
||||
const chalk = require('chalk');
|
||||
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 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]')
|
||||
.allowUnknownOption()
|
||||
.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();
|
||||
const { SERVER_TSCONFIG_PATH } = process.env;
|
||||
process.env.IS_DEV_CMD = true;
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
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()
|
||||
.option('-h, --help')
|
||||
.option('--ts-node-dev')
|
||||
.action((options) => {
|
||||
.action(async (options) => {
|
||||
const cmd = process.argv.slice(2)?.[0];
|
||||
if (cmd === 'install') {
|
||||
await downloadPro();
|
||||
}
|
||||
if (isDev()) {
|
||||
promptForTs();
|
||||
run('tsx', [
|
||||
await run('tsx', [
|
||||
'--tsconfig',
|
||||
SERVER_TSCONFIG_PATH,
|
||||
'-r',
|
||||
@ -32,7 +36,7 @@ module.exports = (cli) => {
|
||||
...process.argv.slice(2),
|
||||
]);
|
||||
} 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)]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -31,6 +31,7 @@ module.exports = (cli) => {
|
||||
require('./umi')(cli);
|
||||
require('./upgrade')(cli);
|
||||
require('./postinstall')(cli);
|
||||
require('./pkg')(cli);
|
||||
if (isPackageValid('@umijs/utils')) {
|
||||
require('./create-plugin')(cli);
|
||||
}
|
||||
|
218
packages/core/cli/src/commands/pkg.js
Normal file
218
packages/core/cli/src/commands/pkg.js
Normal 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...');
|
||||
});
|
||||
};
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
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 { existsSync, mkdirSync, readFileSync, appendFileSync } = require('fs');
|
||||
const { readFile, writeFile } = require('fs').promises;
|
||||
@ -41,7 +41,7 @@ module.exports = (cli) => {
|
||||
.option('--skip-umi')
|
||||
.action(async (options) => {
|
||||
writeToExclude();
|
||||
|
||||
generatePlugins();
|
||||
generatePlaywrightPath(true);
|
||||
await createStoragePluginsSymlink();
|
||||
if (!isDev()) {
|
||||
|
@ -8,10 +8,11 @@
|
||||
*/
|
||||
|
||||
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 { resolve } = require('path');
|
||||
const chalk = require('chalk');
|
||||
const chokidar = require('chokidar');
|
||||
|
||||
function deleteSockFiles() {
|
||||
const { SOCKET_PATH, PM2_HOME } = process.env;
|
||||
@ -38,6 +39,23 @@ module.exports = (cli) => {
|
||||
.option('--quickstart')
|
||||
.allowUnknownOption()
|
||||
.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) {
|
||||
process.env.APP_PORT = opts.port;
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
const chalk = require('chalk');
|
||||
const { Command } = require('commander');
|
||||
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');
|
||||
|
||||
/**
|
||||
@ -29,15 +29,18 @@ module.exports = (cli) => {
|
||||
if (hasTsNode()) promptForTs();
|
||||
if (hasCorePackages()) {
|
||||
// await run('yarn', ['install']);
|
||||
await downloadPro();
|
||||
await runAppCommand('upgrade');
|
||||
return;
|
||||
}
|
||||
if (options.skipCodeUpdate) {
|
||||
await downloadPro();
|
||||
await runAppCommand('upgrade');
|
||||
return;
|
||||
}
|
||||
// await runAppCommand('upgrade');
|
||||
if (!hasTsNode()) {
|
||||
await downloadPro();
|
||||
await runAppCommand('upgrade');
|
||||
return;
|
||||
}
|
||||
@ -54,8 +57,9 @@ module.exports = (cli) => {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
if (pkg.version === stdout) {
|
||||
await downloadPro();
|
||||
await runAppCommand('upgrade');
|
||||
rmAppDir();
|
||||
await rmAppDir();
|
||||
return;
|
||||
}
|
||||
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', ['install']);
|
||||
await downloadPro();
|
||||
await runAppCommand('upgrade');
|
||||
rmAppDir();
|
||||
await rmAppDir();
|
||||
});
|
||||
};
|
||||
|
@ -72,7 +72,7 @@ class PluginGenerator extends Generator {
|
||||
});
|
||||
this.log('');
|
||||
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}`)}`);
|
||||
}
|
||||
}
|
||||
|
@ -163,6 +163,10 @@ exports.promptForTs = () => {
|
||||
console.log(chalk.green('WAIT: ') + 'TypeScript compiling...');
|
||||
};
|
||||
|
||||
exports.downloadPro = async () => {
|
||||
await exports.run('yarn', ['nocobase', 'pkg', 'download-pro']);
|
||||
};
|
||||
|
||||
exports.updateJsonFile = async (target, fn) => {
|
||||
const content = await readFile(target, 'utf-8');
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
@ -135,7 +135,9 @@ export class APIClient extends APIClientSDK {
|
||||
},
|
||||
async (error) => {
|
||||
if (this.silence) {
|
||||
throw error;
|
||||
console.error(error);
|
||||
return;
|
||||
// throw error;
|
||||
}
|
||||
const redirectTo = error?.response?.data?.redirectTo;
|
||||
if (redirectTo) {
|
||||
|
@ -7,14 +7,13 @@
|
||||
* 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 React, { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 { useApp } from '../application';
|
||||
import { PluginDetail } from './PluginDetail';
|
||||
@ -29,8 +28,19 @@ interface IPluginInfo extends IPluginCard {
|
||||
function PluginInfo(props: IPluginInfo) {
|
||||
const { data, onClick } = props;
|
||||
const app = useApp();
|
||||
const { name, displayName, isCompatible, packageName, updatable, builtIn, enabled, description, error, homepage } =
|
||||
data;
|
||||
const {
|
||||
name,
|
||||
displayName,
|
||||
isCompatible,
|
||||
packageName,
|
||||
updatable,
|
||||
builtIn,
|
||||
enabled,
|
||||
removable,
|
||||
description,
|
||||
error,
|
||||
homepage,
|
||||
} = data;
|
||||
const { styles, theme } = useStyles();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
@ -109,30 +119,58 @@ function PluginInfo(props: IPluginInfo) {
|
||||
<ReloadOutlined /> {t('Update')}
|
||||
</a>
|
||||
)}
|
||||
{enabled ? (
|
||||
app.pluginSettingsManager.has(name) && (
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(app.pluginSettingsManager.getRoutePath(name));
|
||||
}}
|
||||
>
|
||||
<SettingOutlined /> {t('Settings')}
|
||||
</a>
|
||||
)
|
||||
) : (
|
||||
{enabled && app.pluginSettingsManager.has(name) && (
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(app.pluginSettingsManager.getRoutePath(name));
|
||||
}}
|
||||
>
|
||||
<SettingOutlined /> {t('Settings')}
|
||||
</a>
|
||||
)}
|
||||
{removable && (
|
||||
<Popconfirm
|
||||
key={'delete'}
|
||||
disabled={builtIn}
|
||||
title={t('Are you sure to delete this plugin?')}
|
||||
onConfirm={async (e) => {
|
||||
e.stopPropagation();
|
||||
api.request({
|
||||
await api.request({
|
||||
url: `pm:remove`,
|
||||
params: {
|
||||
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()}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
|
@ -13,7 +13,6 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRequest } from '../api-client';
|
||||
import { PluginDocument } from './PluginDocument';
|
||||
import { useStyles } from './style';
|
||||
import { IPluginData } from './types';
|
||||
|
||||
@ -121,7 +120,7 @@ export const PluginDetail: FC<IPluginDetail> = ({ plugin, onCancel }) => {
|
||||
children: (
|
||||
<Row gutter={20}>
|
||||
{plugin.name && (
|
||||
<Col span={12}>
|
||||
<Col span={24}>
|
||||
<div className={styles.PluginDetailBaseInfo}>
|
||||
<Typography.Text type="secondary">{t('Name')}</Typography.Text>
|
||||
<Typography.Text strong>{plugin.name}</Typography.Text>
|
||||
@ -129,7 +128,7 @@ export const PluginDetail: FC<IPluginDetail> = ({ plugin, onCancel }) => {
|
||||
</Col>
|
||||
)}
|
||||
{plugin.displayName && (
|
||||
<Col span={12}>
|
||||
<Col span={24}>
|
||||
<div className={styles.PluginDetailBaseInfo}>
|
||||
<Typography.Text type="secondary">{t('DisplayName')}</Typography.Text>
|
||||
<Typography.Text strong>{plugin.displayName}</Typography.Text>
|
||||
@ -169,7 +168,7 @@ export const PluginDetail: FC<IPluginDetail> = ({ plugin, onCancel }) => {
|
||||
</Col>
|
||||
)}
|
||||
{data?.data?.packageJson.license && (
|
||||
<Col span={12}>
|
||||
<Col span={24}>
|
||||
<div className={styles.PluginDetailBaseInfo}>
|
||||
<Typography.Text type="secondary">{t('License')}</Typography.Text>
|
||||
<Typography.Text strong>{data?.data?.packageJson.license}</Typography.Text>
|
||||
@ -177,20 +176,14 @@ export const PluginDetail: FC<IPluginDetail> = ({ plugin, onCancel }) => {
|
||||
</Col>
|
||||
)}
|
||||
{author && (
|
||||
<Col span={12}>
|
||||
<Col span={24}>
|
||||
<div className={styles.PluginDetailBaseInfo}>
|
||||
<Typography.Text type="secondary">{t('Author')}</Typography.Text>
|
||||
<Typography.Text strong>{author}</Typography.Text>
|
||||
</div>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={12}>
|
||||
<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}>
|
||||
<Col span={24}>
|
||||
<div className={styles.PluginDetailBaseInfo}>
|
||||
<Typography.Text type="secondary">{t('Version')}</Typography.Text>
|
||||
<Typography.Text strong>{plugin?.version}</Typography.Text>
|
||||
@ -231,11 +224,11 @@ export const PluginDetail: FC<IPluginDetail> = ({ plugin, onCancel }) => {
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'changelog',
|
||||
label: t('Changelog'),
|
||||
children: plugin?.changelogUrl ? <PluginDocument url={plugin?.changelogUrl} /> : t('No CHANGELOG.md file'),
|
||||
},
|
||||
// {
|
||||
// key: 'changelog',
|
||||
// label: t('Changelog'),
|
||||
// children: plugin?.changelogUrl ? <PluginDocument url={plugin?.changelogUrl} /> : t('No CHANGELOG.md file'),
|
||||
// },
|
||||
];
|
||||
|
||||
return (
|
||||
@ -248,9 +241,6 @@ export const PluginDetail: FC<IPluginDetail> = ({ plugin, onCancel }) => {
|
||||
<Typography.Title level={3}>{plugin.packageName}</Typography.Title>
|
||||
<Space split={<span> • </span>}>
|
||||
<span>{plugin.version}</span>
|
||||
<span>
|
||||
{t('Last updated')} {dayjs(data?.data?.lastUpdated).fromNow()}
|
||||
</span>
|
||||
</Space>
|
||||
<Tabs
|
||||
style={{ minHeight: '50vh' }}
|
||||
|
@ -7,10 +7,11 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { ISchema } from '@formily/json-schema';
|
||||
import { useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { App } from 'antd';
|
||||
import { App, Modal, Result } from 'antd';
|
||||
import type { RcFile } from 'antd/es/upload';
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -19,6 +20,8 @@ import { useAPIClient } from '../../../api-client';
|
||||
import { SchemaComponent } from '../../../schema-component';
|
||||
import { IPluginData } from '../../types';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface IPluginUploadFormProps {
|
||||
onClose: (refresh?: boolean) => void;
|
||||
isUpgrade: boolean;
|
||||
@ -40,12 +43,41 @@ export const PluginUploadForm: FC<IPluginUploadFormProps> = ({ onClose, pluginDa
|
||||
if (pluginData?.packageName) {
|
||||
formData.append('packageName', pluginData.packageName);
|
||||
}
|
||||
api.request({
|
||||
await api.request({
|
||||
url: `pm:${isUpgrade ? 'update' : 'add'}`,
|
||||
method: 'post',
|
||||
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);
|
||||
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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ export const PluginAddModal: FC<IPluginFormProps> = ({ onClose, isShow }) => {
|
||||
const [type, setType] = useState<'npm' | 'upload' | 'url'>('npm');
|
||||
|
||||
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> */}
|
||||
<div style={{ marginTop: theme.marginLG, marginBottom: theme.marginLG }}>
|
||||
<Radio.Group optionType="button" defaultValue={type} onChange={(e) => setType(e.target.value)}>
|
||||
|
@ -197,7 +197,7 @@ const LocalPlugins = () => {
|
||||
<div>
|
||||
<Space>
|
||||
<Button onClick={() => setShowAddForm(true)} type="primary">
|
||||
{t('Add new')}
|
||||
{t('Add & Update')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
@ -16,6 +16,7 @@ export interface IPluginData {
|
||||
packageName: string;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
removable?: boolean;
|
||||
installed: boolean;
|
||||
builtIn: boolean;
|
||||
registry?: string;
|
||||
|
2
packages/core/devtools/umiConfig.d.ts
vendored
2
packages/core/devtools/umiConfig.d.ts
vendored
@ -24,3 +24,5 @@ export declare class IndexGenerator {
|
||||
constructor(outputPath: string, pluginsPath: string[]): void;
|
||||
generate(): void;
|
||||
};
|
||||
|
||||
export declare function generatePlugins(): {}
|
||||
|
@ -137,14 +137,14 @@ class IndexGenerator {
|
||||
generate() {
|
||||
this.generatePluginContent();
|
||||
if (process.env.NODE_ENV === 'production') return;
|
||||
this.pluginsPath.forEach((pluginPath) => {
|
||||
if (!fs.existsSync(pluginPath)) {
|
||||
return;
|
||||
}
|
||||
fs.watch(pluginPath, { recursive: false }, () => {
|
||||
this.generatePluginContent();
|
||||
});
|
||||
});
|
||||
// this.pluginsPath.forEach((pluginPath) => {
|
||||
// if (!fs.existsSync(pluginPath)) {
|
||||
// return;
|
||||
// }
|
||||
// fs.watch(pluginPath, { recursive: false }, () => {
|
||||
// this.generatePluginContent();
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
get indexContent() {
|
||||
@ -156,7 +156,11 @@ function devDynamicImport(packageName: string): Promise<any> {
|
||||
if (!fileName) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return import(\`./packages/\${fileName}\`)
|
||||
try {
|
||||
return import(\`./packages/\${fileName}\`)
|
||||
} catch (error) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
export default devDynamicImport;`;
|
||||
}
|
||||
@ -170,9 +174,9 @@ export default function devDynamicImport(packageName: string): Promise<any> {
|
||||
|
||||
generatePluginContent() {
|
||||
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));
|
||||
if (!validPluginPaths.length || process.env.NODE_ENV === 'production') {
|
||||
fs.writeFileSync(this.indexPath, this.emptyIndexContent);
|
||||
@ -247,3 +251,13 @@ export default function devDynamicImport(packageName: string): Promise<any> {
|
||||
exports.getUmiConfig = getUmiConfig;
|
||||
exports.resolveNocobasePackagesAlias = resolveNocobasePackagesAlias;
|
||||
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();
|
||||
};
|
||||
|
@ -7,10 +7,10 @@
|
||||
* 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 { PluginManager } from '../plugin-manager';
|
||||
import { vi } from 'vitest';
|
||||
import { MockServer, mockServer } from '@nocobase/test';
|
||||
|
||||
describe('pm', () => {
|
||||
let app: MockServer;
|
||||
@ -216,7 +216,40 @@ describe('pm', () => {
|
||||
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;
|
||||
PluginManager.resolvePlugin = async (pluginName) => {
|
||||
return Plugin1;
|
||||
@ -239,26 +272,72 @@ describe('pm', () => {
|
||||
await app.cleanDb();
|
||||
await app.load();
|
||||
await app.install();
|
||||
await app.pm.repository.create({
|
||||
values: {
|
||||
name: 'Plugin1',
|
||||
},
|
||||
});
|
||||
await app.reload();
|
||||
await app.pm.enable('Plugin1');
|
||||
expect(loadFn).toBeCalledTimes(3);
|
||||
expect(app.pm.get('Plugin1').enabled).toBeTruthy();
|
||||
expect(app.pm.get('Plugin1').installed).toBeTruthy();
|
||||
expect(loadFn).toBeCalled();
|
||||
expect(loadFn).toBeCalledTimes(3);
|
||||
await app.pm.enable('Plugin1');
|
||||
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.enable('Plugin1');
|
||||
expect(loadFn).toBeCalledTimes(5);
|
||||
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();
|
||||
|
||||
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 {
|
||||
async beforeEnable() {
|
||||
loadFn();
|
||||
@ -270,6 +349,7 @@ describe('pm', () => {
|
||||
loadFn();
|
||||
}
|
||||
}
|
||||
|
||||
class Plugin2 extends Plugin {
|
||||
async beforeEnable() {
|
||||
loadFn();
|
||||
@ -281,39 +361,33 @@ describe('pm', () => {
|
||||
loadFn();
|
||||
}
|
||||
}
|
||||
const resolvePlugin = PluginManager.resolvePlugin;
|
||||
PluginManager.resolvePlugin = async (pluginName: string) => {
|
||||
return {
|
||||
Plugin1,
|
||||
Plugin2,
|
||||
}[pluginName];
|
||||
};
|
||||
|
||||
const loadFn = vi.fn();
|
||||
|
||||
app = mockServer();
|
||||
await app.cleanDb();
|
||||
await app.load();
|
||||
await app.install();
|
||||
await app.pm.repository.create({
|
||||
values: [
|
||||
{
|
||||
name: 'Plugin1',
|
||||
},
|
||||
{
|
||||
name: 'Plugin2',
|
||||
},
|
||||
],
|
||||
});
|
||||
await app.reload();
|
||||
await app.pm.enable(['Plugin1', 'Plugin2']);
|
||||
expect(loadFn).toBeCalledTimes(6);
|
||||
expect(app.pm.get('Plugin1').enabled).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']);
|
||||
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;
|
||||
});
|
||||
|
||||
test('disable', async () => {
|
||||
const resolvePlugin = PluginManager.resolvePlugin;
|
||||
PluginManager.resolvePlugin = async (pluginName) => {
|
||||
@ -338,10 +412,9 @@ describe('pm', () => {
|
||||
},
|
||||
});
|
||||
await app.reload();
|
||||
expect(app.pm.get('Plugin1').enabled).toBeFalsy();
|
||||
expect(app.pm.get('Plugin1').installed).toBeFalsy();
|
||||
await app.pm.disable('Plugin1');
|
||||
expect(app.pm.get('Plugin1')).toBeUndefined();
|
||||
expect(loadFn).not.toBeCalled();
|
||||
await expect(() => app.pm.disable('Plugin1')).rejects.toThrow('Plugin1 plugin does not exist');
|
||||
PluginManager.resolvePlugin = resolvePlugin;
|
||||
});
|
||||
test('disable', async () => {
|
||||
@ -375,8 +448,13 @@ describe('pm', () => {
|
||||
await app.pm.disable('Plugin1');
|
||||
expect(loadFn).toBeCalled();
|
||||
expect(loadFn).toBeCalledTimes(2);
|
||||
expect(app.pm.get('Plugin1').enabled).toBeFalsy();
|
||||
expect(app.pm.get('Plugin1').installed).toBeTruthy();
|
||||
const instance = await app.pm.repository.findOne({
|
||||
filter: {
|
||||
name: 'Plugin1',
|
||||
},
|
||||
});
|
||||
expect(instance.enabled).toBeFalsy();
|
||||
expect(instance.installed).toBeTruthy();
|
||||
PluginManager.resolvePlugin = resolvePlugin;
|
||||
});
|
||||
test('install', async () => {
|
||||
@ -464,22 +542,22 @@ describe('pm', () => {
|
||||
await app.cleanDb();
|
||||
await app.load();
|
||||
await app.install();
|
||||
const plugin = await app.pm.repository.create({
|
||||
await app.pm.repository.create({
|
||||
values: {
|
||||
name: 'Plugin1',
|
||||
},
|
||||
});
|
||||
await app.reload();
|
||||
expect(app.pm.get('Plugin1')['prop']).toBeUndefined();
|
||||
expect(app.pm.has('Plugin1')).toBeFalsy();
|
||||
expect(result).toEqual([]);
|
||||
await app.pm.enable('Plugin1');
|
||||
expect(app.pm.get('Plugin1')['prop']).toBe('a');
|
||||
// console.log(hooks.join('/'));
|
||||
expect(result).toEqual([false, true, true]);
|
||||
expect(result).toEqual([true, true, true]);
|
||||
await app.pm.disable('Plugin1');
|
||||
// console.log(hooks.join('/'));
|
||||
expect(app.pm.get('Plugin1')['prop']).toBeUndefined();
|
||||
expect(result).toEqual([false, true, true, true, false]);
|
||||
expect(app.pm.has('Plugin1')).toBeFalsy();
|
||||
expect(result).toEqual([true, true, true, true, true]);
|
||||
// console.log(hooks.join('/'));
|
||||
PluginManager.resolvePlugin = resolvePlugin;
|
||||
});
|
||||
|
@ -9,8 +9,7 @@
|
||||
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { fsExists } from '@nocobase/utils';
|
||||
import fs from 'fs';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
import Application from '../application';
|
||||
import { ApplicationNotInstall } from '../errors/application-not-install';
|
||||
@ -23,12 +22,16 @@ export default (app: Application) => {
|
||||
.option('--quickstart')
|
||||
.action(async (...cliArgs) => {
|
||||
const [options] = cliArgs;
|
||||
const file = resolve(process.cwd(), 'storage/app-upgrading');
|
||||
const upgrading = await fsExists(file);
|
||||
const file = resolve(process.cwd(), 'storage/.upgrading');
|
||||
const upgrading = await fs.exists(file);
|
||||
if (upgrading) {
|
||||
await app.upgrade();
|
||||
if (!process.env.VITEST) {
|
||||
if (await app.isInstalled()) {
|
||||
await app.upgrade();
|
||||
}
|
||||
}
|
||||
try {
|
||||
await fs.promises.rm(file);
|
||||
await fs.rm(file, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
// skip
|
||||
}
|
||||
|
@ -123,7 +123,9 @@ export default {
|
||||
async list(ctx, next) {
|
||||
const locale = ctx.getCurrentLocale();
|
||||
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();
|
||||
},
|
||||
async listEnabled(ctx, next) {
|
||||
@ -156,7 +158,9 @@ export default {
|
||||
if (!filterByTk) {
|
||||
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();
|
||||
},
|
||||
},
|
||||
|
@ -24,6 +24,8 @@ export class PluginManagerRepository extends Repository {
|
||||
this.pm = pm;
|
||||
}
|
||||
|
||||
async createByName(nameOrPkgs) {}
|
||||
|
||||
async has(nameOrPkg: string) {
|
||||
const { name } = await PluginManager.parseName(nameOrPkg);
|
||||
const instance = await this.findOne({
|
||||
@ -79,11 +81,19 @@ export class PluginManagerRepository extends Repository {
|
||||
}
|
||||
|
||||
async updateVersions() {
|
||||
const items = await this.find();
|
||||
const items = await this.find({
|
||||
filter: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
for (const item of items) {
|
||||
const json = await PluginManager.getPackageJson(item.packageName);
|
||||
item.set('version', json.version);
|
||||
await item.save();
|
||||
try {
|
||||
const json = await PluginManager.getPackageJson(item.packageName);
|
||||
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({
|
||||
sort: 'id',
|
||||
filter: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -10,10 +10,9 @@
|
||||
import Topo from '@hapi/topo';
|
||||
import { CleanOptions, Collection, SyncOptions } from '@nocobase/database';
|
||||
import { importModule, isURL } from '@nocobase/utils';
|
||||
import { fsExists } from '@nocobase/utils/plugin-symlink';
|
||||
import execa from 'execa';
|
||||
import fg from 'fast-glob';
|
||||
import fs from 'fs';
|
||||
import fs from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
import net from 'net';
|
||||
import { basename, join, resolve, sep } from 'path';
|
||||
@ -26,12 +25,12 @@ import resourceOptions from './options/resource';
|
||||
import { PluginManagerRepository } from './plugin-manager-repository';
|
||||
import { PluginData } from './types';
|
||||
import {
|
||||
checkAndGetCompatible,
|
||||
copyTempPackageToStorageAndLinkToNodeModules,
|
||||
downloadAndUnzipToTempDir,
|
||||
getNpmInfo,
|
||||
getPluginBasePath,
|
||||
getPluginInfoByNpm,
|
||||
removeTmpDir,
|
||||
updatePluginByCompressedFileUrl,
|
||||
} from './utils';
|
||||
|
||||
@ -56,6 +55,8 @@ export interface InstallOptions {
|
||||
export class AddPresetError extends Error {}
|
||||
|
||||
export class PluginManager {
|
||||
static checkAndGetCompatible = checkAndGetCompatible;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -118,12 +119,19 @@ export class PluginManager {
|
||||
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
|
||||
*/
|
||||
static async getPackageJson(packageName: string) {
|
||||
const file = await fs.promises.realpath(resolve(process.env.NODE_MODULES_PATH, packageName, 'package.json'));
|
||||
const data = await fs.promises.readFile(file, { encoding: 'utf-8' });
|
||||
static async getPackageJson(nameOrPkg: string) {
|
||||
const { packageName } = await this.parseName(nameOrPkg);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -134,7 +142,7 @@ export class PluginManager {
|
||||
const prefixes = this.getPluginPkgPrefix();
|
||||
for (const prefix of prefixes) {
|
||||
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) {
|
||||
return `${prefix}${name}`;
|
||||
}
|
||||
@ -196,9 +204,7 @@ export class PluginManager {
|
||||
*/
|
||||
static async resolvePlugin(pluginName: string | typeof Plugin, isUpgrade = false, isPkg = false) {
|
||||
if (typeof pluginName === 'string') {
|
||||
const packageName = isPkg ? pluginName : await this.getPackageName(pluginName);
|
||||
this.clearCache(packageName);
|
||||
|
||||
const { packageName } = await this.parseName(pluginName);
|
||||
return await importModule(packageName);
|
||||
} else {
|
||||
return pluginName;
|
||||
@ -226,7 +232,7 @@ export class PluginManager {
|
||||
return this.parsedNames[nameOrPkg];
|
||||
}
|
||||
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'),
|
||||
);
|
||||
};
|
||||
@ -285,7 +291,7 @@ export class PluginManager {
|
||||
const createPlugin = async (name) => {
|
||||
const pluginDir = resolve(process.cwd(), 'packages/plugins', name);
|
||||
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 generator = new PluginGenerator({
|
||||
@ -298,16 +304,6 @@ export class PluginManager {
|
||||
await generator.run();
|
||||
};
|
||||
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');
|
||||
const { name, packageName } = await PluginManager.parseName(pluginName);
|
||||
const json = await PluginManager.getPackageJson(packageName);
|
||||
@ -316,15 +312,6 @@ export class PluginManager {
|
||||
packageName,
|
||||
version: json.version,
|
||||
});
|
||||
await this.repository.updateOrCreate({
|
||||
values: {
|
||||
name,
|
||||
packageName,
|
||||
version: json.version,
|
||||
},
|
||||
filterKeys: ['name'],
|
||||
});
|
||||
await sleep(1000);
|
||||
await tsxRerunning();
|
||||
}
|
||||
|
||||
@ -374,14 +361,6 @@ export class PluginManager {
|
||||
if (options.packageName) {
|
||||
this.pluginAliases.set(options.packageName, instance);
|
||||
}
|
||||
if (insert && options.name) {
|
||||
await this.repository.updateOrCreate({
|
||||
values: {
|
||||
...options,
|
||||
},
|
||||
filterKeys: ['name'],
|
||||
});
|
||||
}
|
||||
await instance.afterAdd();
|
||||
}
|
||||
|
||||
@ -524,16 +503,69 @@ export class PluginManager {
|
||||
async enable(nameOrPkg: string | string[]) {
|
||||
let pluginNames = nameOrPkg;
|
||||
if (nameOrPkg === '*') {
|
||||
const items = await this.repository.find();
|
||||
pluginNames = items.map((item: any) => item.name);
|
||||
const plugin = this.get('nocobase') as any;
|
||||
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.setMaintainingMessage(`enabling plugin ${pluginNames.join(',')}`);
|
||||
const toBeUpdated = [];
|
||||
for (const name of pluginNames) {
|
||||
const { name: pluginName } = await PluginManager.parseName(name);
|
||||
console.log('pluginName', pluginName);
|
||||
const plugin = this.get(pluginName);
|
||||
if (!plugin) {
|
||||
throw new Error(`${pluginName} plugin does not exist`);
|
||||
@ -544,7 +576,6 @@ export class PluginManager {
|
||||
await this.app.emitAsync('beforeEnablePlugin', pluginName);
|
||||
try {
|
||||
await plugin.beforeEnable();
|
||||
plugin.enabled = true;
|
||||
toBeUpdated.push(pluginName);
|
||||
} catch (error) {
|
||||
if (nameOrPkg === '*') {
|
||||
@ -557,16 +588,7 @@ export class PluginManager {
|
||||
if (toBeUpdated.length === 0) {
|
||||
return;
|
||||
}
|
||||
await this.repository.update({
|
||||
filter: {
|
||||
name: toBeUpdated,
|
||||
},
|
||||
values: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
try {
|
||||
await this.app.reload();
|
||||
this.app.log.debug(`syncing database in enable plugin ${toBeUpdated.join(',')}...`);
|
||||
this.app.setMaintainingMessage(`syncing database in enable plugin ${toBeUpdated.join(',')}...`);
|
||||
await this.app.db.sync();
|
||||
@ -579,32 +601,31 @@ export class PluginManager {
|
||||
plugin.installed = true;
|
||||
}
|
||||
}
|
||||
await this.repository.update({
|
||||
filter: {
|
||||
name: toBeUpdated,
|
||||
},
|
||||
values: {
|
||||
for (const pluginName of toBeUpdated) {
|
||||
const { name } = await PluginManager.parseName(pluginName);
|
||||
const packageJson = await PluginManager.getPackageJson(pluginName);
|
||||
const values = {
|
||||
name,
|
||||
packageName: packageJson?.name,
|
||||
enabled: true,
|
||||
installed: true,
|
||||
},
|
||||
});
|
||||
version: packageJson?.version,
|
||||
};
|
||||
await this.repository.updateOrCreate({
|
||||
values,
|
||||
filterKeys: ['name'],
|
||||
});
|
||||
}
|
||||
for (const pluginName of toBeUpdated) {
|
||||
const plugin = this.get(pluginName);
|
||||
this.app.log.debug(`emit afterEnablePlugin event...`);
|
||||
await plugin.afterEnable();
|
||||
plugin.enabled = true;
|
||||
await this.app.emitAsync('afterEnablePlugin', pluginName);
|
||||
this.app.log.debug(`afterEnablePlugin event emitted`);
|
||||
}
|
||||
await this.app.tryReloadOrRestart();
|
||||
} catch (error) {
|
||||
await this.repository.update({
|
||||
filter: {
|
||||
name: toBeUpdated,
|
||||
},
|
||||
values: {
|
||||
enabled: false,
|
||||
installed: false,
|
||||
},
|
||||
});
|
||||
await this.app.tryReloadOrRestart({
|
||||
recover: true,
|
||||
});
|
||||
@ -634,32 +655,24 @@ export class PluginManager {
|
||||
if (toBeUpdated.length === 0) {
|
||||
return;
|
||||
}
|
||||
await this.repository.update({
|
||||
filter: {
|
||||
name: toBeUpdated,
|
||||
},
|
||||
values: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
try {
|
||||
await this.app.tryReloadOrRestart();
|
||||
for (const pluginName of pluginNames) {
|
||||
for (const pluginName of toBeUpdated) {
|
||||
const plugin = this.get(pluginName);
|
||||
this.app.log.debug(`emit afterDisablePlugin event...`);
|
||||
await plugin.afterDisable();
|
||||
await this.app.emitAsync('afterDisablePlugin', pluginName);
|
||||
this.app.log.debug(`afterDisablePlugin event emitted`);
|
||||
}
|
||||
} catch (error) {
|
||||
await this.repository.update({
|
||||
filter: {
|
||||
name: toBeUpdated,
|
||||
},
|
||||
values: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
await this.app.tryReloadOrRestart();
|
||||
} catch (error) {
|
||||
await this.app.tryReloadOrRestart({
|
||||
recover: true,
|
||||
});
|
||||
@ -684,72 +697,43 @@ export class PluginManager {
|
||||
records.map(async (plugin) => {
|
||||
const dir = resolve(process.env.NODE_MODULES_PATH, plugin.packageName);
|
||||
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}`);
|
||||
return fs.promises.rm(realDir, { force: true, recursive: true });
|
||||
return fs.rm(realDir, { force: true, recursive: true });
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
);
|
||||
await execa('yarn', ['nocobase', 'postinstall']);
|
||||
};
|
||||
if (options?.force) {
|
||||
await this.repository.destroy({
|
||||
filter: {
|
||||
name: pluginNames,
|
||||
},
|
||||
});
|
||||
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 this.repository.destroy({
|
||||
filter: {
|
||||
name: pluginNames,
|
||||
},
|
||||
});
|
||||
if (!this.app.db.getCollection('applications')) {
|
||||
await removeDir();
|
||||
}
|
||||
await execa('yarn', ['nocobase', 'refresh'], {
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
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)) {
|
||||
for (const packageName of urlOrName) {
|
||||
await this.addViaCLI(packageName, _.omit(options, 'name'), false);
|
||||
}
|
||||
await this.app.emitStartedEvent();
|
||||
await execa('yarn', ['nocobase', 'postinstall']);
|
||||
return;
|
||||
}
|
||||
if (isURL(urlOrName)) {
|
||||
@ -760,7 +744,7 @@ export class PluginManager {
|
||||
},
|
||||
emitStartedEvent,
|
||||
);
|
||||
} else if (await fsExists(urlOrName)) {
|
||||
} else if (await fs.exists(urlOrName)) {
|
||||
await this.addByCompressedFileUrl(
|
||||
{
|
||||
...(options as any),
|
||||
@ -778,20 +762,6 @@ export class PluginManager {
|
||||
},
|
||||
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 { 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);
|
||||
return this.add(name, { packageName }, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -861,19 +819,7 @@ export class PluginManager {
|
||||
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);
|
||||
return this.add(name, { packageName }, true);
|
||||
}
|
||||
|
||||
async update(nameOrPkg: string | string[], options: PluginData, emitStartedEvent = true) {
|
||||
@ -888,7 +834,7 @@ export class PluginManager {
|
||||
return;
|
||||
}
|
||||
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 tsxRerunning();
|
||||
await execa('yarn', ['nocobase', 'pm2-restart'], {
|
||||
@ -904,7 +850,7 @@ export class PluginManager {
|
||||
const opts = { ...options };
|
||||
if (isURL(nameOrPkg)) {
|
||||
opts.compressedFileUrl = nameOrPkg;
|
||||
} else if (await fsExists(nameOrPkg)) {
|
||||
} else if (await fs.exists(nameOrPkg)) {
|
||||
opts.compressedFileUrl = nameOrPkg;
|
||||
}
|
||||
if (opts.compressedFileUrl) {
|
||||
@ -958,7 +904,7 @@ export class PluginManager {
|
||||
repository: this.repository,
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
private sort(names: string | string[]) {
|
||||
private async sort(names: string | string[]) {
|
||||
const pluginNames = _.castArray(names);
|
||||
if (pluginNames.length === 1) {
|
||||
return pluginNames;
|
||||
}
|
||||
const sorter = new Topo.Sorter<string>();
|
||||
for (const pluginName of pluginNames) {
|
||||
const plugin = this.get(pluginName);
|
||||
const peerDependencies = Object.keys(plugin.options?.packageJson?.peerDependencies || {});
|
||||
sorter.add(pluginName, { after: peerDependencies, group: plugin.options?.packageName || pluginName });
|
||||
const packageJson = await PluginManager.getPackageJson(pluginName);
|
||||
const peerDependencies = Object.keys(packageJson?.peerDependencies || {});
|
||||
sorter.add(pluginName, { after: peerDependencies, group: packageJson?.packageName || pluginName });
|
||||
}
|
||||
return sorter.nodes;
|
||||
}
|
||||
|
@ -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 new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
expect((await getSubAppMapRecord(sub1)).get('enabled')).toBeTruthy();
|
||||
expect((await getSubAppMapRecord(sub1)).enabled).toBeTruthy();
|
||||
// create new app sub2
|
||||
await mainApp.db.getRepository('applications').create({
|
||||
values: {
|
||||
|
@ -127,7 +127,7 @@ export class MultiAppShareCollectionPlugin extends Plugin {
|
||||
throw new Error('multi-app-share-collection plugin only support postgres');
|
||||
}
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
@ -66,8 +66,58 @@
|
||||
"@nocobase/plugin-workflow-request": "1.4.0-alpha",
|
||||
"@nocobase/plugin-workflow-sql": "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": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nocobase/nocobase.git",
|
||||
|
114
packages/presets/nocobase/src/server/findPackageNames.ts
Normal file
114
packages/presets/nocobase/src/server/findPackageNames.ts
Normal 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;
|
||||
}
|
@ -9,88 +9,78 @@
|
||||
|
||||
import { Plugin, PluginManager } from '@nocobase/server';
|
||||
import _ from 'lodash';
|
||||
import { findBuiltInPlugins, findLocalPlugins, trim } from './findPackageNames';
|
||||
|
||||
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) {
|
||||
return (name || '').split(',').filter(Boolean);
|
||||
}
|
||||
|
||||
getBuiltInPlugins() {
|
||||
const { APPEND_PRESET_BUILT_IN_PLUGINS } = process.env;
|
||||
return _.uniq(this.splitNames(APPEND_PRESET_BUILT_IN_PLUGINS).concat(this.builtInPlugins));
|
||||
async getBuiltInPlugins() {
|
||||
return await findBuiltInPlugins();
|
||||
}
|
||||
|
||||
getLocalPlugins() {
|
||||
const { APPEND_PRESET_LOCAL_PLUGINS } = process.env;
|
||||
const plugins = this.splitNames(APPEND_PRESET_LOCAL_PLUGINS)
|
||||
.concat(this.localPlugins)
|
||||
.map((name) => name.split('>='));
|
||||
return plugins;
|
||||
async getLocalPlugins() {
|
||||
return [];
|
||||
return (await findLocalPlugins()).map((name) => name.split('>='));
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -100,9 +90,11 @@ export class PresetNocoBase extends Plugin {
|
||||
}
|
||||
|
||||
async allPlugins() {
|
||||
const builtInPlugins = await this.getBuiltInPlugins();
|
||||
const localPlugins = await this.getLocalPlugins();
|
||||
return (
|
||||
await Promise.all(
|
||||
this.getBuiltInPlugins().map(async (pkgOrName) => {
|
||||
builtInPlugins.map(async (pkgOrName) => {
|
||||
const { name } = await PluginManager.parseName(pkgOrName);
|
||||
const packageJson = await this.getPackageJson(pkgOrName);
|
||||
return {
|
||||
@ -116,7 +108,7 @@ export class PresetNocoBase extends Plugin {
|
||||
)
|
||||
).concat(
|
||||
await Promise.all(
|
||||
this.getLocalPlugins().map(async (plugin) => {
|
||||
localPlugins.map(async (plugin) => {
|
||||
const { name } = await PluginManager.parseName(plugin[0]);
|
||||
const packageJson = await this.getPackageJson(plugin[0]);
|
||||
return { name, packageName: packageJson.name, version: packageJson.version };
|
||||
@ -128,8 +120,10 @@ export class PresetNocoBase extends Plugin {
|
||||
async getPluginToBeUpgraded() {
|
||||
const repository = this.app.db.getRepository<any>('applicationPlugins');
|
||||
const items = (await repository.find()).map((item) => item.name);
|
||||
const builtInPlugins = await this.getBuiltInPlugins();
|
||||
const localPlugins = await this.getLocalPlugins();
|
||||
const plugins = await Promise.all(
|
||||
this.getBuiltInPlugins().map(async (pkgOrName) => {
|
||||
builtInPlugins.map(async (pkgOrName) => {
|
||||
const { name } = await PluginManager.parseName(pkgOrName);
|
||||
const packageJson = await this.getPackageJson(pkgOrName);
|
||||
return {
|
||||
@ -141,7 +135,7 @@ export class PresetNocoBase extends Plugin {
|
||||
} as any;
|
||||
}),
|
||||
);
|
||||
for (const plugin of this.getLocalPlugins()) {
|
||||
for (const plugin of localPlugins) {
|
||||
if (plugin[1]) {
|
||||
// 不在插件列表,并且插件最低版本小于当前应用版本,跳过不处理
|
||||
if (!items.includes(plugin[0]) && (await this.app.version.satisfies(`>${plugin[1]}`))) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user