454 lines
15 KiB
TypeScript
454 lines
15 KiB
TypeScript
import fs from 'fs';
|
||
import path from 'path';
|
||
import glob from 'fast-glob';
|
||
import { program, Command } from '@/program.ts';
|
||
import { getConfig, query } from '@/module/index.ts';
|
||
import { fileIsExist } from '@/uitls/file.ts';
|
||
import { chalk } from '@/module/chalk.ts';
|
||
import * as backServices from '@/query/services/index.ts';
|
||
import { input } from '@inquirer/prompts';
|
||
import { logger } from '@/module/logger.ts';
|
||
|
||
// 查找文件(忽略大小写)
|
||
async function findFileInsensitive(targetFile: string): Promise<string | null> {
|
||
const files = fs.readdirSync('.');
|
||
const matchedFile = files.find((file) => file.toLowerCase() === targetFile.toLowerCase());
|
||
return matchedFile || null;
|
||
}
|
||
|
||
// 递归收集文件信息
|
||
async function collectFileInfo(filePath: string, baseDir = '.'): Promise<any[]> {
|
||
const stats = fs.statSync(filePath);
|
||
const relativePath = path.relative(baseDir, filePath);
|
||
|
||
if (stats.isFile()) {
|
||
return [{ path: relativePath, size: stats.size }];
|
||
}
|
||
|
||
if (stats.isDirectory()) {
|
||
const files = fs.readdirSync(filePath);
|
||
const results = await Promise.all(files.map((file) => collectFileInfo(path.join(filePath, file), baseDir)));
|
||
return results.flat();
|
||
}
|
||
|
||
return [];
|
||
}
|
||
/**
|
||
* 复制文件到 pack-dist
|
||
* @param files 文件列表, 或者文件夹列表
|
||
* @param cwd 当前工作目录
|
||
* @param packDist 打包目录 pack-dist
|
||
* @param mergeDist 是否合并 dist 目录到 pack-dist 中
|
||
*/
|
||
export const copyFilesToPackDist = async (files: string[], cwd: string, packDist = 'pack-dist', mergeDist = true) => {
|
||
const packDistPath = path.join(cwd, packDist);
|
||
if (!fileIsExist(packDistPath)) {
|
||
fs.mkdirSync(packDistPath, { recursive: true });
|
||
} else {
|
||
fs.rmSync(packDistPath, { recursive: true, force: true });
|
||
}
|
||
files.forEach((file) => {
|
||
const stat = fs.statSync(path.join(cwd, file));
|
||
let outputFile = file;
|
||
if (mergeDist) {
|
||
if (file.startsWith('dist/')) {
|
||
outputFile = file.replace(/^dist\//, '');
|
||
} else if (file === 'dist') {
|
||
outputFile = '';
|
||
}
|
||
}
|
||
if (stat.isDirectory()) {
|
||
fs.cpSync(path.join(cwd, file), path.join(packDistPath, outputFile), { recursive: true });
|
||
} else {
|
||
fs.copyFileSync(path.join(cwd, file), path.join(packDistPath, outputFile));
|
||
}
|
||
});
|
||
const packageInfo = await getPackageInfo();
|
||
// 根据所有文件,生成一个index.html
|
||
const indexHtmlPath = path.join(packDistPath, 'index.html');
|
||
const collectionFiles = (await Promise.all(files.map((file) => collectFileInfo(file)))).flat();
|
||
const prettifySize = (size: number) => {
|
||
if (size < 1024) {
|
||
return `${size}B`;
|
||
}
|
||
if (size < 1024 * 1024) {
|
||
return `${(size / 1024).toFixed(2)}kB`;
|
||
}
|
||
return `${(size / 1024 / 1024).toFixed(2)}MB`;
|
||
};
|
||
const filesString = collectionFiles.map((file) => `<li><a href="${file.path}">${file.path}</a><span>${prettifySize(file.size)}</span></li>`).join('\n');
|
||
const indexHtmlContent = `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
|
||
<head>
|
||
<title>${packageInfo.name}</title>
|
||
</head>
|
||
|
||
<body>
|
||
<h1>${packageInfo.name}</h1>
|
||
<ul>
|
||
${filesString}
|
||
</ul>
|
||
<pre>${JSON.stringify(packageInfo, null, 2)}</pre>
|
||
</body>
|
||
|
||
</html>`;
|
||
if (!fileIsExist(indexHtmlPath)) {
|
||
fs.writeFileSync(indexHtmlPath, indexHtmlContent);
|
||
}
|
||
};
|
||
|
||
export const pack = async (opts: { packDist?: string, mergeDist?: boolean }) => {
|
||
const cwd = process.cwd();
|
||
const collection: Record<string, any> = {};
|
||
const mergeDist = opts.mergeDist !== false;
|
||
const packageJsonPath = path.join(cwd, 'package.json');
|
||
if (!fileIsExist(packageJsonPath)) {
|
||
console.error('package.json not found');
|
||
return;
|
||
}
|
||
|
||
let packageJson;
|
||
try {
|
||
const packageContent = fs.readFileSync(packageJsonPath, 'utf-8');
|
||
packageJson = JSON.parse(packageContent);
|
||
} catch (error) {
|
||
console.error('Invalid package.json:', error);
|
||
return;
|
||
}
|
||
let files = packageJson.files;
|
||
|
||
// 从 package.json 的 files 字段收集文件
|
||
const filesToInclude = files
|
||
? await glob(files, {
|
||
cwd: cwd,
|
||
dot: true, // 包括隐藏文件
|
||
onlyFiles: false, // 包括目录
|
||
followSymbolicLinks: true, // 处理符号链接
|
||
ignore: ['node_modules/**', ".git/**", opts.packDist ? opts.packDist + '/**' : ''],
|
||
})
|
||
: [];
|
||
// 确保 README.md 和 dist 存在(忽略大小写检测 README.md)
|
||
const readmeFile = await findFileInsensitive('README.md');
|
||
if (readmeFile && !filesToInclude.includes(readmeFile)) {
|
||
filesToInclude.push(readmeFile);
|
||
}
|
||
const packageFile = await findFileInsensitive('package.json');
|
||
if (packageFile && !filesToInclude.includes(packageFile)) {
|
||
filesToInclude.push(packageFile);
|
||
}
|
||
const allFiles = (await Promise.all(filesToInclude.map((file) => collectFileInfo(file)))).flat();
|
||
|
||
// 输出文件详细信息
|
||
logger.debug('文件列表:');
|
||
allFiles.forEach((file) => {
|
||
logger.debug(`${file.size}B ${file.path}`);
|
||
});
|
||
const totalSize = allFiles.reduce((sum, file) => sum + file.size, 0);
|
||
|
||
collection.files = allFiles;
|
||
collection.packageJson = packageJson;
|
||
collection.totalSize = totalSize;
|
||
collection.tags = packageJson.app?.tags || packageJson.keywords || [];
|
||
|
||
logger.debug('\n基本信息');
|
||
logger.debug(`name: ${packageJson.name}`);
|
||
logger.debug(`version: ${packageJson.version}`);
|
||
logger.debug(`total files: ${allFiles.length}`);
|
||
try {
|
||
copyFilesToPackDist(filesToInclude, cwd, opts.packDist, mergeDist);
|
||
} catch (error) {
|
||
console.error('Error creating tarball:', error);
|
||
}
|
||
const readme = await findFileInsensitive('README.md');
|
||
if (readme) {
|
||
const readmeContent = fs.readFileSync(readme, 'utf-8');
|
||
collection.readme = readmeContent;
|
||
}
|
||
return { collection, dir: cwd };
|
||
};
|
||
export const getPackageInfo = async () => {
|
||
const cwd = process.cwd();
|
||
const packageJsonPath = path.join(cwd, 'package.json');
|
||
try {
|
||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||
return packageJson;
|
||
} catch (error) {
|
||
console.error('Invalid package.json:', error);
|
||
return {};
|
||
}
|
||
};
|
||
|
||
const publishCommand = new Command('publish')
|
||
.description('发布应用')
|
||
.option('-k, --key <key>', '应用 key')
|
||
.option('-v, --version <version>', '应用版本')
|
||
.action(async (options) => {
|
||
const { key, version } = options;
|
||
const config = await getConfig();
|
||
console.log('发布逻辑实现', { key, version, config });
|
||
});
|
||
|
||
const deployLoadFn = async (id: string, fileKey: string, force = true, install = false) => {
|
||
if (!id) {
|
||
console.error(chalk.red('id is required'));
|
||
return;
|
||
}
|
||
let appKey = '';
|
||
let version = '';
|
||
if (id && id.includes('/')) {
|
||
const [a, b] = id.split('/');
|
||
if (a) {
|
||
appKey = b || '1.0.0';
|
||
version = a;
|
||
id = '';
|
||
} else {
|
||
console.error(chalk.red('id format error, please use "version/appKey" format'));
|
||
return;
|
||
}
|
||
}
|
||
const res = await query.post({
|
||
path: 'micro-app',
|
||
key: 'deploy',
|
||
data: {
|
||
id: id,
|
||
version: version,
|
||
appKey: appKey,
|
||
key: fileKey,
|
||
force: force,
|
||
install: !!install,
|
||
},
|
||
});
|
||
if (res.code === 200) {
|
||
console.log('deploy-load success. current version:', res.data?.pkg?.version);
|
||
console.log('run: ', 'envision services -s', res.data?.showAppInfo?.key);
|
||
} else {
|
||
console.error('deploy-load 失败', res.message);
|
||
}
|
||
return res;
|
||
};
|
||
|
||
const packCommand = new Command('pack')
|
||
.description('打包应用, 使用 package.json 中的 files 字段,如果是 dist 的路径,直接复制到 pack-dist 的根目录')
|
||
.option('-p, --publish', '打包并发布')
|
||
.option('-u, --update', '发布后显示更新命令, show command for deploy to server')
|
||
.option('-d, --packDist <dist>', '打包到的目录')
|
||
.option('-m, --mergeDist <mergeDist>', '合并 dist 目录到 pack-dist 中', "true")
|
||
.option('-y, --yes <yes>', '确定,直接打包', "true")
|
||
.option('-c, --clean', '清理 package.json中的 devDependencies')
|
||
.action(async (opts) => {
|
||
const packDist = opts.packDist || 'pack-dist';
|
||
const mergeDist = opts.mergeDist === "true";
|
||
const yes = opts.yes === "true";
|
||
const packageInfo = await getPackageInfo();
|
||
if (!packageInfo) {
|
||
console.error('Invalid package.json:');
|
||
return;
|
||
}
|
||
let basename = packageInfo.basename || '';
|
||
let appKey: string | undefined;
|
||
let version = packageInfo.version || '';
|
||
if (!version) {
|
||
version = await input({
|
||
message: 'Enter your version:',
|
||
});
|
||
}
|
||
|
||
if (basename) {
|
||
if (basename.startsWith('/')) {
|
||
basename = basename.slice(1);
|
||
}
|
||
const basenameArr = basename.split('/');
|
||
if (basenameArr.length !== 2) {
|
||
console.error(chalk.red('basename is error, 请输入正确的路径, packages.json中basename例如 root/appKey'));
|
||
return;
|
||
}
|
||
appKey = basenameArr[1] || '';
|
||
}
|
||
if (!appKey) {
|
||
appKey = await input({
|
||
message: 'Enter your appKey:',
|
||
});
|
||
}
|
||
let value = await pack({
|
||
packDist,
|
||
mergeDist
|
||
});
|
||
if (opts?.clean) {
|
||
const newPackageJson = { ...packageInfo };
|
||
delete newPackageJson.devDependencies;
|
||
fs.writeFileSync(path.join(process.cwd(), 'pack-dist', 'package.json'), JSON.stringify(newPackageJson, null, 2));
|
||
}
|
||
if (opts.publish) {
|
||
// 运行 deploy 命令
|
||
// const runDeployCommand = 'envision pack-deploy ' + value.outputFilePath + ' -k ' + appKey;
|
||
const [_app, _command] = process.argv;
|
||
let deployDist = packDist;
|
||
const deployCommand = [_app, _command, 'deploy', deployDist, '-k', appKey, '-v', version, '-u', '-d'];
|
||
if (opts.org) {
|
||
deployCommand.push('-o', opts.org);
|
||
}
|
||
if (opts.update) {
|
||
deployCommand.push('-s');
|
||
}
|
||
if (yes) {
|
||
deployCommand.push('-y', 'yes');
|
||
}
|
||
logger.debug(chalk.blue('deploy doing: '), deployCommand.slice(2).join(' '), '\n');
|
||
// console.log('pack deploy services', chalk.blue('example: '), runDeployCommand);
|
||
|
||
program.parse(deployCommand);
|
||
}
|
||
});
|
||
const packDeployCommand = new Command('pack-deploy')
|
||
.argument('<id>', 'id')
|
||
.option('-k, --key <key>', 'fileKey, 服务器的部署文件夹的列表')
|
||
.option('-i, --install ', 'install dependencies')
|
||
.action(async (id, opts) => {
|
||
let { key, install } = opts || {};
|
||
const res = await deployLoadFn(id, key, true, install);
|
||
});
|
||
|
||
program.addCommand(packDeployCommand);
|
||
program.addCommand(publishCommand);
|
||
program.addCommand(packCommand);
|
||
|
||
enum AppType {
|
||
/**
|
||
* run in (import way)
|
||
*/
|
||
SystemApp = 'system-app',
|
||
/**
|
||
* fork 执行
|
||
*/
|
||
MicroApp = 'micro-app',
|
||
GatewayApp = 'gateway-app',
|
||
/**
|
||
* pm2 启动
|
||
*/
|
||
Pm2SystemApp = 'pm2-system-app',
|
||
/**
|
||
* 脚本应用
|
||
*/
|
||
ScriptApp = 'script-app',
|
||
}
|
||
type ServiceItem = {
|
||
key: string;
|
||
status: 'inactive' | 'running' | 'stop' | 'error' | 'unknown';
|
||
type: AppType;
|
||
description: string;
|
||
version: string;
|
||
};
|
||
|
||
const servicesCommand = new Command('services')
|
||
.description('服务器registry当中的服务管理')
|
||
.option('-l, --list', 'list services')
|
||
.option('-r, --restart <service>', 'restart services')
|
||
.option('-s, --start <service>', 'start services')
|
||
.option('-t, --stop <service>', 'stop services')
|
||
.option('-i, --info <services>', 'info services')
|
||
.option('-d, --delete <services>', 'delete services')
|
||
.action(async (opts) => {
|
||
//
|
||
if (opts.list) {
|
||
const res = await backServices.queryServiceList();
|
||
if (res.code === 200) {
|
||
// console.log('res', JSON.stringify(res.data, null, 2));
|
||
const data = res.data as ServiceItem[];
|
||
console.log('services list');
|
||
const getMaxLengths = (data) => {
|
||
const lengths = { key: 0, status: 0, type: 0, description: 0, version: 0 };
|
||
data.forEach((item) => {
|
||
lengths.key = Math.max(lengths.key, item.key.length);
|
||
lengths.status = Math.max(lengths.status, item.status.length);
|
||
lengths.type = Math.max(lengths.type, item.type.length);
|
||
lengths.description = Math.max(lengths.description, item.description.length);
|
||
lengths.version = Math.max(lengths.version, item.version.length);
|
||
});
|
||
return lengths;
|
||
};
|
||
const lengths = getMaxLengths(data);
|
||
const padString = (str, length) => str + ' '.repeat(Math.max(length - str.length, 0));
|
||
try {
|
||
console.log(
|
||
chalk.blue(padString('Key', lengths.key)),
|
||
chalk.green(padString('Status', lengths.status)),
|
||
chalk.yellow(padString('Type', lengths.type)),
|
||
chalk.red(padString('Version', lengths.version)),
|
||
);
|
||
} catch (error) {
|
||
console.error('error', error);
|
||
}
|
||
data.forEach((item) => {
|
||
console.log(
|
||
chalk.blue(padString(item.key, lengths.key)),
|
||
chalk.green(padString(item.status, lengths.status)),
|
||
chalk.blue(padString(item.type, lengths.type)),
|
||
chalk.green(padString(item.version, lengths.version)),
|
||
);
|
||
});
|
||
} else {
|
||
console.log('error', chalk.red(res.message || '获取列表失败'));
|
||
}
|
||
return;
|
||
}
|
||
if (opts.restart) {
|
||
const res = await backServices.queryServiceOperate(opts.restart, 'restart');
|
||
if (res.code === 200) {
|
||
console.log('restart success');
|
||
} else {
|
||
console.error('restart failed', res.message);
|
||
}
|
||
return;
|
||
}
|
||
if (opts.start) {
|
||
const res = await backServices.queryServiceOperate(opts.start, 'start');
|
||
if (res.code === 200) {
|
||
console.log('start success');
|
||
} else {
|
||
console.error('start failed', res.message);
|
||
}
|
||
return;
|
||
}
|
||
if (opts.stop) {
|
||
const res = await backServices.queryServiceOperate(opts.stop, 'stop');
|
||
if (res.code === 200) {
|
||
console.log('stop success');
|
||
} else {
|
||
console.log(chalk.red('stop failed'), res.message);
|
||
}
|
||
return;
|
||
}
|
||
if (opts.info) {
|
||
const res = await backServices.queryServiceList();
|
||
if (res.code === 200) {
|
||
const data = res.data as ServiceItem[];
|
||
const item = data.find((item) => item.key === opts.info);
|
||
if (!item) {
|
||
console.log('not found');
|
||
return;
|
||
}
|
||
console.log(chalk.blue(item.key), chalk.green(item.status), chalk.yellow(item.type), chalk.red(item.version));
|
||
console.log('description:', chalk.blue(item.description));
|
||
} else {
|
||
console.log(chalk.red(res.message || '获取列表失败'));
|
||
}
|
||
}
|
||
if (opts.delete) {
|
||
// const res = await backServices.queryServiceOperate(opts.delete, 'delete');
|
||
const res = await backServices.queryServiceDelect(opts.delete);
|
||
if (res.code === 200) {
|
||
console.log('delete success');
|
||
} else {
|
||
console.log(chalk.red('delete failed'), res.message);
|
||
}
|
||
}
|
||
});
|
||
const detectCommand = new Command('detect').description('检测服务, 当返回内容不为true,则是有新增的内容').action(async () => {
|
||
const res = await backServices.queryServiceDetect();
|
||
console.log('detect', res);
|
||
});
|
||
program.addCommand(servicesCommand);
|
||
servicesCommand.addCommand(detectCommand);
|