Files
cli/src/command/publish.ts
2026-01-22 17:31:26 +08:00

454 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);