mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
483 lines
14 KiB
JavaScript
483 lines
14 KiB
JavaScript
/**
|
||
* 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 net = require('net');
|
||
const chalk = require('chalk');
|
||
const execa = require('execa');
|
||
const fg = require('fast-glob');
|
||
const { dirname, join, resolve, sep, isAbsolute } = require('path');
|
||
const { readFile, writeFile } = require('fs').promises;
|
||
const { existsSync, mkdirSync, cpSync, writeFileSync } = require('fs');
|
||
const dotenv = require('dotenv');
|
||
const fs = require('fs-extra');
|
||
const os = require('os');
|
||
const moment = require('moment-timezone');
|
||
|
||
exports.isPackageValid = (pkg) => {
|
||
try {
|
||
require.resolve(pkg);
|
||
return true;
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
exports.hasCorePackages = () => {
|
||
const coreDir = resolve(process.cwd(), 'packages/core/build');
|
||
return existsSync(coreDir);
|
||
};
|
||
|
||
exports.hasTsNode = () => {
|
||
return exports.isPackageValid('ts-node/dist/bin');
|
||
};
|
||
|
||
exports.isDev = function isDev() {
|
||
if (process.env.APP_ENV === 'production') {
|
||
return false;
|
||
}
|
||
return exports.hasTsNode();
|
||
};
|
||
|
||
const isProd = () => {
|
||
const { APP_PACKAGE_ROOT } = process.env;
|
||
const file = `${APP_PACKAGE_ROOT}/lib/index.js`;
|
||
if (!existsSync(resolve(process.cwd(), file))) {
|
||
console.log('For production environment, please build the code first.');
|
||
console.log();
|
||
console.log(chalk.yellow('$ yarn build'));
|
||
console.log();
|
||
process.exit(1);
|
||
}
|
||
return true;
|
||
};
|
||
|
||
exports.isProd = isProd;
|
||
|
||
exports.nodeCheck = () => {
|
||
if (!exports.hasTsNode()) {
|
||
console.log('Please install all dependencies');
|
||
console.log(chalk.yellow('$ yarn install'));
|
||
process.exit(1);
|
||
}
|
||
};
|
||
|
||
exports.run = (command, args, options = {}) => {
|
||
if (command === 'tsx') {
|
||
command = 'node';
|
||
args = ['./node_modules/tsx/dist/cli.mjs'].concat(args || []);
|
||
}
|
||
return execa(command, args, {
|
||
shell: true,
|
||
stdio: 'inherit',
|
||
...options,
|
||
env: {
|
||
...process.env,
|
||
...options.env,
|
||
},
|
||
});
|
||
};
|
||
|
||
exports.isPortReachable = async (port, { timeout = 1000, host } = {}) => {
|
||
const promise = new Promise((resolve, reject) => {
|
||
const socket = new net.Socket();
|
||
|
||
const onError = () => {
|
||
socket.destroy();
|
||
reject();
|
||
};
|
||
|
||
socket.setTimeout(timeout);
|
||
socket.once('error', onError);
|
||
socket.once('timeout', onError);
|
||
|
||
socket.connect(port, host, () => {
|
||
socket.end();
|
||
resolve();
|
||
});
|
||
});
|
||
|
||
try {
|
||
await promise;
|
||
return true;
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
exports.postCheck = async (opts) => {
|
||
const port = opts.port || process.env.APP_PORT;
|
||
const result = await exports.isPortReachable(port);
|
||
if (result) {
|
||
console.error(chalk.red(`post already in use ${port}`));
|
||
process.exit(1);
|
||
}
|
||
};
|
||
|
||
exports.runInstall = async () => {
|
||
const { APP_PACKAGE_ROOT, SERVER_TSCONFIG_PATH } = process.env;
|
||
|
||
if (exports.isDev()) {
|
||
const argv = [
|
||
'--tsconfig',
|
||
SERVER_TSCONFIG_PATH,
|
||
'-r',
|
||
'tsconfig-paths/register',
|
||
`${APP_PACKAGE_ROOT}/src/index.ts`,
|
||
'install',
|
||
'-s',
|
||
];
|
||
await exports.run('tsx', argv);
|
||
} else if (isProd()) {
|
||
const file = `${APP_PACKAGE_ROOT}/lib/index.js`;
|
||
const argv = [file, 'install', '-s'];
|
||
await exports.run('node', argv);
|
||
}
|
||
};
|
||
|
||
exports.runAppCommand = async (command, args = []) => {
|
||
const { APP_PACKAGE_ROOT, SERVER_TSCONFIG_PATH } = process.env;
|
||
|
||
if (exports.isDev()) {
|
||
const argv = [
|
||
'--tsconfig',
|
||
SERVER_TSCONFIG_PATH,
|
||
'-r',
|
||
'tsconfig-paths/register',
|
||
`${APP_PACKAGE_ROOT}/src/index.ts`,
|
||
command,
|
||
...args,
|
||
];
|
||
await exports.run('tsx', argv);
|
||
} else if (isProd()) {
|
||
const argv = [`${APP_PACKAGE_ROOT}/lib/index.js`, command, ...args];
|
||
await exports.run('node', argv);
|
||
}
|
||
};
|
||
|
||
exports.promptForTs = () => {
|
||
console.log(chalk.green('WAIT: ') + 'TypeScript compiling...');
|
||
};
|
||
|
||
exports.downloadPro = async () => {
|
||
// 此处不再判定,由pkgg命令处理
|
||
// const { NOCOBASE_PKG_USERNAME, NOCOBASE_PKG_PASSWORD } = process.env;
|
||
// if (!(NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD)) {
|
||
// return;
|
||
// }
|
||
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);
|
||
await writeFile(target, JSON.stringify(fn(json), null, 2), 'utf-8');
|
||
};
|
||
|
||
exports.getVersion = async () => {
|
||
const { stdout } = await execa('npm', ['v', '@nocobase/app-server', 'versions']);
|
||
const versions = new Function(`return (${stdout})`)();
|
||
return versions[versions.length - 1];
|
||
};
|
||
|
||
exports.generateAppDir = function generateAppDir() {
|
||
const appPkgPath = dirname(dirname(require.resolve('@nocobase/app/src/index.ts')));
|
||
const appDevDir = resolve(process.cwd(), './storage/.app-dev');
|
||
if (exports.isDev() && !exports.hasCorePackages() && appPkgPath.includes('node_modules')) {
|
||
if (!existsSync(appDevDir)) {
|
||
mkdirSync(appDevDir, { force: true, recursive: true });
|
||
cpSync(appPkgPath, appDevDir, {
|
||
recursive: true,
|
||
force: true,
|
||
});
|
||
}
|
||
process.env.APP_PACKAGE_ROOT = appDevDir;
|
||
} else {
|
||
process.env.APP_PACKAGE_ROOT = appPkgPath;
|
||
}
|
||
buildIndexHtml();
|
||
};
|
||
|
||
exports.genTsConfigPaths = function genTsConfigPaths() {
|
||
try {
|
||
fs.unlinkSync(resolve(process.cwd(), 'node_modules/.bin/tsx'));
|
||
fs.symlinkSync(
|
||
resolve(process.cwd(), 'node_modules/tsx/dist/cli.mjs'),
|
||
resolve(process.cwd(), 'node_modules/.bin/tsx'),
|
||
'file',
|
||
);
|
||
} catch (error) {
|
||
//
|
||
}
|
||
|
||
const cwd = process.cwd();
|
||
const cwdLength = cwd.length;
|
||
const paths = {
|
||
'@@/*': ['.dumi/tmp/*'],
|
||
};
|
||
const packages = fg.sync(['packages/*/*/package.json', 'packages/*/*/*/package.json'], {
|
||
absolute: true,
|
||
onlyFiles: true,
|
||
});
|
||
packages.forEach((packageFile) => {
|
||
const packageJsonName = require(packageFile).name;
|
||
const packageDir = dirname(packageFile);
|
||
const relativePath = packageDir
|
||
.slice(cwdLength + 1)
|
||
.split(sep)
|
||
.join('/');
|
||
paths[`${packageJsonName}/client`] = [`${relativePath}/src/client`];
|
||
paths[`${packageJsonName}/package.json`] = [`${relativePath}/package.json`];
|
||
paths[packageJsonName] = [`${relativePath}/src`];
|
||
if (packageJsonName === '@nocobase/test') {
|
||
paths[`${packageJsonName}/server`] = [`${relativePath}/src/server`];
|
||
paths[`${packageJsonName}/e2e`] = [`${relativePath}/src/e2e`];
|
||
paths[`${packageJsonName}/web`] = [`${relativePath}/src/web`];
|
||
}
|
||
if (packageJsonName === '@nocobase/client') {
|
||
paths[`${packageJsonName}/demo-utils`] = [`${relativePath}/src/demo-utils`];
|
||
}
|
||
if (packageJsonName === '@nocobase/plugin-workflow-test') {
|
||
paths[`${packageJsonName}/e2e`] = [`${relativePath}/src/e2e`];
|
||
}
|
||
});
|
||
|
||
const tsConfigJsonPath = join(cwd, './tsconfig.paths.json');
|
||
const content = { compilerOptions: { paths } };
|
||
writeFileSync(tsConfigJsonPath, JSON.stringify(content, null, 2), 'utf-8');
|
||
return content;
|
||
};
|
||
|
||
function generatePlaywrightPath(clean = false) {
|
||
try {
|
||
const playwright = resolve(process.cwd(), 'storage/playwright/tests');
|
||
if (clean && fs.existsSync(playwright)) {
|
||
fs.rmSync(dirname(playwright), { force: true, recursive: true });
|
||
}
|
||
if (!fs.existsSync(playwright)) {
|
||
const testPkg = require.resolve('@nocobase/test/package.json');
|
||
fs.cpSync(resolve(dirname(testPkg), 'playwright/tests'), playwright, { recursive: true });
|
||
}
|
||
} catch (error) {
|
||
// empty
|
||
}
|
||
}
|
||
|
||
exports.generatePlaywrightPath = generatePlaywrightPath;
|
||
|
||
function parseEnv(name) {
|
||
if (name === 'DB_UNDERSCORED') {
|
||
if (process.env.DB_UNDERSCORED === 'true') {
|
||
return 'true';
|
||
}
|
||
if (process.env.DB_UNDERSCORED) {
|
||
return 'true';
|
||
}
|
||
return 'false';
|
||
}
|
||
}
|
||
|
||
function buildIndexHtml(force = false) {
|
||
const file = `${process.env.APP_PACKAGE_ROOT}/dist/client/index.html`;
|
||
if (!fs.existsSync(file)) {
|
||
return;
|
||
}
|
||
const tpl = `${process.env.APP_PACKAGE_ROOT}/dist/client/index.html.tpl`;
|
||
if (force && fs.existsSync(tpl)) {
|
||
fs.rmSync(tpl);
|
||
}
|
||
if (!fs.existsSync(tpl)) {
|
||
fs.copyFileSync(file, tpl);
|
||
}
|
||
const data = fs.readFileSync(tpl, 'utf-8');
|
||
const replacedData = data
|
||
.replace(/\{\{env.APP_PUBLIC_PATH\}\}/g, process.env.APP_PUBLIC_PATH)
|
||
.replace(/\{\{env.API_CLIENT_STORAGE_TYPE\}\}/g, process.env.API_CLIENT_STORAGE_TYPE)
|
||
.replace(/\{\{env.API_CLIENT_STORAGE_PREFIX\}\}/g, process.env.API_CLIENT_STORAGE_PREFIX)
|
||
.replace(/\{\{env.API_BASE_URL\}\}/g, process.env.API_BASE_URL || process.env.API_BASE_PATH)
|
||
.replace(/\{\{env.WS_URL\}\}/g, process.env.WEBSOCKET_URL || '')
|
||
.replace(/\{\{env.WS_PATH\}\}/g, process.env.WS_PATH)
|
||
.replace('src="/umi.', `src="${process.env.APP_PUBLIC_PATH}umi.`);
|
||
fs.writeFileSync(file, replacedData, 'utf-8');
|
||
}
|
||
|
||
exports.buildIndexHtml = buildIndexHtml;
|
||
|
||
function getTimezonesByOffset(offset) {
|
||
if (!/^[+-]\d{1,2}:\d{2}$/.test(offset)) {
|
||
return offset;
|
||
}
|
||
const offsetMinutes = moment.duration(offset).asMinutes();
|
||
return moment.tz.names().find((timezone) => {
|
||
return moment.tz(timezone).utcOffset() === offsetMinutes;
|
||
});
|
||
}
|
||
|
||
function areTimeZonesEqual(timeZone1, timeZone2) {
|
||
if (timeZone1 === timeZone2) {
|
||
return true;
|
||
}
|
||
timeZone1 = getTimezonesByOffset(timeZone1);
|
||
timeZone2 = getTimezonesByOffset(timeZone2);
|
||
return moment.tz(timeZone1).format('Z') === moment.tz(timeZone2).format('Z');
|
||
}
|
||
|
||
function generateGatewayPath() {
|
||
if (process.env.SOCKET_PATH) {
|
||
if (isAbsolute(process.env.SOCKET_PATH)) {
|
||
return process.env.SOCKET_PATH;
|
||
}
|
||
return resolve(process.cwd(), process.env.SOCKET_PATH);
|
||
}
|
||
if (process.env.NOCOBASE_RUNNING_IN_DOCKER === 'true') {
|
||
return resolve(os.homedir(), '.nocobase', 'gateway.sock');
|
||
}
|
||
return resolve(process.cwd(), 'storage/gateway.sock');
|
||
}
|
||
|
||
function generatePm2Home() {
|
||
if (process.env.PM2_HOME) {
|
||
if (isAbsolute(process.env.PM2_HOME)) {
|
||
return process.env.PM2_HOME;
|
||
}
|
||
return resolve(process.cwd(), process.env.PM2_HOME);
|
||
}
|
||
if (process.env.NOCOBASE_RUNNING_IN_DOCKER === 'true') {
|
||
return resolve(os.homedir(), '.nocobase', 'pm2');
|
||
}
|
||
return resolve(process.cwd(), './storage/.pm2');
|
||
}
|
||
|
||
exports.initEnv = function initEnv() {
|
||
const env = {
|
||
APP_ENV: 'development',
|
||
APP_KEY: 'test-jwt-secret',
|
||
APP_PORT: 13000,
|
||
API_BASE_PATH: '/api/',
|
||
API_CLIENT_STORAGE_PREFIX: 'NOCOBASE_',
|
||
API_CLIENT_STORAGE_TYPE: 'localStorage',
|
||
DB_DIALECT: 'sqlite',
|
||
DB_STORAGE: 'storage/db/nocobase.sqlite',
|
||
// DB_TIMEZONE: '+00:00',
|
||
DB_UNDERSCORED: parseEnv('DB_UNDERSCORED'),
|
||
DEFAULT_STORAGE_TYPE: 'local',
|
||
LOCAL_STORAGE_DEST: 'storage/uploads',
|
||
PLUGIN_STORAGE_PATH: resolve(process.cwd(), 'storage/plugins'),
|
||
MFSU_AD: 'none',
|
||
MAKO_AD: 'none',
|
||
WS_PATH: '/ws',
|
||
// PM2_HOME: generatePm2Home(),
|
||
// SOCKET_PATH: generateGatewayPath(),
|
||
NODE_MODULES_PATH: resolve(process.cwd(), 'node_modules'),
|
||
PLUGIN_PACKAGE_PREFIX: '@nocobase/plugin-,@nocobase/plugin-sample-,@nocobase/preset-',
|
||
SERVER_TSCONFIG_PATH: './tsconfig.server.json',
|
||
PLAYWRIGHT_AUTH_FILE: resolve(process.cwd(), 'storage/playwright/.auth/admin.json'),
|
||
CACHE_DEFAULT_STORE: 'memory',
|
||
CACHE_MEMORY_MAX: 2000,
|
||
BROWSERSLIST_IGNORE_OLD_DATA: true,
|
||
PLUGIN_STATICS_PATH: '/static/plugins/',
|
||
LOGGER_BASE_PATH: 'storage/logs',
|
||
APP_SERVER_BASE_URL: '',
|
||
APP_PUBLIC_PATH: '/',
|
||
WATCH_FILE: resolve(process.cwd(), 'storage/app.watch.ts'),
|
||
};
|
||
|
||
if (
|
||
!process.env.APP_ENV_PATH &&
|
||
process.argv[2] &&
|
||
['test', 'test:client', 'test:server'].includes(process.argv[2])
|
||
) {
|
||
if (fs.existsSync(resolve(process.cwd(), '.env.test'))) {
|
||
process.env.APP_ENV_PATH = '.env.test';
|
||
}
|
||
}
|
||
|
||
if (!process.env.APP_ENV_PATH && process.argv[2] === 'e2e') {
|
||
// 用于存放 playwright 自动生成的相关的文件
|
||
generatePlaywrightPath();
|
||
if (!fs.existsSync('.env.e2e') && fs.existsSync('.env.e2e.example')) {
|
||
const env = fs.readFileSync('.env.e2e.example');
|
||
fs.writeFileSync('.env.e2e', env);
|
||
}
|
||
if (!fs.existsSync('.env.e2e')) {
|
||
throw new Error('Please create .env.e2e file first!');
|
||
}
|
||
process.env.APP_ENV_PATH = '.env.e2e';
|
||
}
|
||
|
||
dotenv.config({
|
||
path: resolve(process.cwd(), process.env.APP_ENV_PATH || '.env'),
|
||
});
|
||
|
||
if (process.argv[2] === 'e2e' && !process.env.APP_BASE_URL) {
|
||
process.env.APP_BASE_URL = `http://127.0.0.1:${process.env.APP_PORT}`;
|
||
}
|
||
|
||
for (const key in env) {
|
||
if (!process.env[key]) {
|
||
process.env[key] = env[key];
|
||
}
|
||
}
|
||
|
||
if (!process.env.__env_modified__ && process.env.APP_PUBLIC_PATH) {
|
||
const publicPath = process.env.APP_PUBLIC_PATH.replace(/\/$/g, '');
|
||
const keys = ['API_BASE_PATH', 'WS_PATH', 'PLUGIN_STATICS_PATH'];
|
||
for (const key of keys) {
|
||
process.env[key] = publicPath + process.env[key];
|
||
}
|
||
process.env.__env_modified__ = true;
|
||
}
|
||
|
||
if (!process.env.__env_modified__ && process.env.APP_SERVER_BASE_URL && !process.env.API_BASE_URL) {
|
||
process.env.API_BASE_URL = process.env.APP_SERVER_BASE_URL + process.env.API_BASE_PATH;
|
||
process.env.__env_modified__ = true;
|
||
}
|
||
|
||
if (!process.env.TZ) {
|
||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||
process.env.TZ = getTimezonesByOffset(process.env.DB_TIMEZONE || timeZone);
|
||
}
|
||
|
||
if (!process.env.DB_TIMEZONE) {
|
||
process.env.DB_TIMEZONE = process.env.TZ;
|
||
}
|
||
|
||
if (!/^[+-]\d{1,2}:\d{2}$/.test(process.env.DB_TIMEZONE)) {
|
||
process.env.DB_TIMEZONE = moment.tz(process.env.DB_TIMEZONE).format('Z');
|
||
}
|
||
|
||
if (!areTimeZonesEqual(process.env.DB_TIMEZONE, process.env.TZ)) {
|
||
throw new Error(
|
||
`process.env.DB_TIMEZONE="${process.env.DB_TIMEZONE}" and process.env.TZ="${process.env.TZ}" are different`,
|
||
);
|
||
}
|
||
|
||
process.env.PM2_HOME = generatePm2Home();
|
||
process.env.SOCKET_PATH = generateGatewayPath();
|
||
fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true });
|
||
fs.mkdirpSync(process.env.PM2_HOME, { force: true, recursive: true });
|
||
};
|
||
|
||
exports.generatePlugins = function () {
|
||
try {
|
||
require.resolve('@nocobase/devtools/umiConfig');
|
||
const { generatePlugins } = require('@nocobase/devtools/umiConfig');
|
||
generatePlugins();
|
||
} catch (error) {
|
||
return;
|
||
}
|
||
};
|
||
|
||
exports.getKey = function () {
|
||
const keyFile = resolve(process.cwd(), './.key');
|
||
if (!fs.existsSync(keyFile)) {
|
||
return;
|
||
}
|
||
return fs.readFileSync(keyFile, 'utf-8');
|
||
};
|