import fs, { createReadStream } from 'fs'; import path from 'path'; import * as tar from 'tar'; 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 ignore from 'ignore'; import { chalk } from '@/module/chalk.ts'; import * as backServices from '@/query/services/index.ts'; import inquirer from 'inquirer'; // 查找文件(忽略大小写) async function findFileInsensitive(targetFile: string): Promise { const files = fs.readdirSync('.'); const matchedFile = files.find((file) => file.toLowerCase() === targetFile.toLowerCase()); return matchedFile || null; } // 递归收集文件信息 async function collectFileInfo(filePath: string, baseDir = '.'): Promise { 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 []; } // 解析 .npmignore 文件 async function loadNpmIgnore(cwd: string): Promise { const npmIgnorePath = path.join(cwd, '.npmignore'); const ig = ignore(); try { const content = fs.readFileSync(npmIgnorePath, 'utf-8'); ig.add(content); } catch (err) { console.warn('.npmignore not found, using default ignore rules'); // 如果没有 .npmignore 文件,使用默认规则 ig.add(['node_modules', '.git']); } return ig; } // 获取文件列表,兼容 .npmignore async function getFiles(cwd: string, patterns: string[]): Promise { const ig = await loadNpmIgnore(cwd); // 使用 fast-glob 匹配文件 const allFiles = await glob(patterns, { cwd, dot: true, // 包括隐藏文件 onlyFiles: false, // 包括目录 followSymbolicLinks: true, }); // 过滤忽略的文件 const filteredFiles = allFiles.filter((file) => !ig.ignores(file)); return filteredFiles; } /** * 复制文件到 pack-dist * @param files 文件列表 * @param cwd 当前工作目录 * @param packDist 打包目录 */ export const copyFilesToPackDist = async (files: string[], cwd: string, packDist = 'pack-dist') => { 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)); if (stat.isDirectory()) { fs.cpSync(path.join(cwd, file), path.join(packDistPath, file), { recursive: true }); } else { fs.copyFileSync(path.join(cwd, file), path.join(packDistPath, file), fs.constants.COPYFILE_EXCL); } }); 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) => `
  • ${file.path}${prettifySize(file.size)}
  • `).join('\n'); const indexHtmlContent = ` ${packageInfo.name}

    ${packageInfo.name}

      ${filesString}
    ${JSON.stringify(packageInfo, null, 2)}
    `; fs.writeFileSync(indexHtmlPath, indexHtmlContent); }; export const pack = async (opts: { isTar: boolean; packDist?: string }) => { const cwd = process.cwd(); const collection: Record = {}; 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 outputFileName = `${packageJson.name}-${packageJson.version}.tgz` .replace('@', '') // 替换特殊字符 @ .replace(/[\/\\:*?"<>|]/g, '-'); // 替换特殊字符 // 当 opts.isTar 为 true 时,输出文件为 tgz 文件 const outputFilePath = path.join(cwd, outputFileName); // 从 package.json 的 files 字段收集文件 const filesToInclude = packageJson.files ? await glob(packageJson.files, { cwd: cwd, dot: true, // 包括隐藏文件 onlyFiles: false, // 包括目录 followSymbolicLinks: true, // 处理符号链接 }) : []; // 确保 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(); // 输出文件详细信息 console.log('Tarball Contents:'); allFiles.forEach((file) => { console.log(`${file.size}B ${file.path}`); }); const totalSize = allFiles.reduce((sum, file) => sum + file.size, 0); const packageSize = (totalSize / 1024).toFixed(2) + ' kB'; collection.files = allFiles; collection.packageJson = packageJson; collection.totalSize = totalSize; collection.tags = packageJson.app?.tags || packageJson.keywords || []; console.log('\nTarball Details'); console.log(`name: ${packageJson.name}`); console.log(`version: ${packageJson.version}`); console.log(`filename: ${outputFileName}`); console.log(`package size: ${packageSize}`); console.log(`total files: ${allFiles.length}`); opts?.isTar && console.log(`Created package: ${outputFileName}`); try { if (opts.isTar) { await tar.c( { gzip: true, file: outputFilePath, cwd: cwd, }, filesToInclude, ); } else { copyFilesToPackDist(filesToInclude, cwd, opts.packDist); } } 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, outputFilePath }; }; 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 {}; } }; type PackByIgnoreOpts = { isTar: boolean; packDist?: string; }; export const packByIgnore = async (opts: PackByIgnoreOpts) => { let collection: Record = {}; const cwd = process.cwd(); const patterns = ['**/*']; // 匹配所有文件 const packageJsonPath = path.join(cwd, 'package.json'); let packageJson; try { const packageContent = fs.readFileSync(packageJsonPath, 'utf-8'); packageJson = JSON.parse(packageContent); } catch (error) { console.error('Invalid package.json:', error); return; } let outputFileName = `${packageJson.name}-${packageJson.version}.tgz` .replace('@', '') // 替换特殊字符 @ .replace(/[\/\\:*?"<>|]/g, '-'); // 替换特殊字符 const outputFilePath = path.join(cwd, outputFileName); // 获取符合条件的文件列表 const files = await getFiles(cwd, patterns); console.log('Files to include in the package:'); // files 获取 size 和 path const filesInfo = await Promise.all(files.map((file) => collectFileInfo(file))); const allFiles = filesInfo.flat(); allFiles.forEach((file) => { console.log(`${file.size}B ${file.path}`); }); const totalSize = allFiles.reduce((sum, file) => sum + file.size, 0); const packageSize = (totalSize / 1024).toFixed(2) + ' kB'; collection.files = allFiles; collection.packageJson = packageJson; collection.totalSize = totalSize; collection.tags = packageJson.app?.tags || packageJson.keywords || []; console.log('\nTarball Details'); console.log(`package size: ${packageSize}`); console.log(`total files: ${allFiles.length}`); const filesToInclude = files.map((file) => path.relative(cwd, file)); try { if (opts.isTar) { await tar.c( { gzip: true, file: outputFilePath, cwd: cwd, }, filesToInclude, ); } else { copyFilesToPackDist(filesToInclude, cwd, opts.packDist); } } 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, outputFilePath }; }; /** * 打包应用 * @param ignore 是否忽略 .npmignore 文件 * @returns 打包结果 */ export const packLib = async ({ ignore = false, tar = false, packDist = 'pack-dist' }: { ignore?: boolean; tar?: boolean; packDist?: string }) => { if (ignore) { return await packByIgnore({ isTar: tar, packDist }); } return await pack({ isTar: tar, packDist }); }; export const unpackLib = async (filePath: string, cwd: string) => { try { await tar.x({ file: filePath, cwd: cwd, }); } catch (error) { console.error('Error extracting tarball:', error); } }; const publishCommand = new Command('publish') .description('发布应用') .option('-k, --key ', '应用 key') .option('-v, --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 = false, install = false) => { if (!id) { console.error(chalk.red('id is required')); return; } // pkg: { // name: 'mark', // version: '0.0.2', // description: '', // main: 'dist/app.mjs', // app: [Object], // files: [Array], // scripts: [Object], // keywords: [Array], // author: 'abearxiong ', // license: 'MIT', // type: 'module', // devDependencies: [Object], // dependencies: [Object] // }, const res = await query.post({ path: 'micro-app', key: 'deploy', data: { id: id, 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?.pkg?.app?.name || res.data?.pkg?.name); } else { console.error('deploy-load failed', res.message); } return res; }; const packCommand = new Command('pack') .description('打包应用, 默认使用 package.json 中的 files 字段') .option('-i, --ignore', '使用 .npmignore 文件模式去忽略文件进行打包, 不需要package.json中的files字段') .option('-p, --publish', '打包并发布') .option('-u, --update', '发布后显示更新命令, show command for deploy to server') .option('-t, --tar', '打包为 tgz 文件') .option('-d, --packDist ', '打包目录') .option('-y, --yes', '确定,直接打包', true) .action(async (opts) => { const packDist = opts.packDist || 'pack-dist'; 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) { const answers = await inquirer.prompt([ { type: 'input', name: 'version', message: 'Enter your version:', }, ]); version = answers.version || 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) { const answers = await inquirer.prompt([ { type: 'input', name: 'appKey', message: 'Enter your appKey:', }, ]); appKey = answers.appKey || appKey; } let value: { collection: Record; outputFilePath: string } = await packLib({ ignore: opts.ignore, tar: opts.tar, packDist, }); if (opts.publish) { // 运行 deploy 命令 const runDeployCommand = 'envision pack-deploy ' + value.outputFilePath + ' -k ' + appKey; const [_app, _command] = process.argv; console.log(chalk.blue('example: '), runDeployCommand); let deployDist = opts.isTar ? value.outputFilePath : packDist; const deployCommand = [_app, _command, 'deploy', deployDist, '-k', appKey, '-v', version, '-u']; if (opts.org) { deployCommand.push('-o', opts.org); } if (opts.update) { deployCommand.push('-s'); } if (opts.yes) { deployCommand.push('-y', 'yes'); } program.parse(deployCommand); } }); const packDeployCommand = new Command('pack-deploy') .argument('', 'id') .option('-k, --key ', 'fileKey, 服务器的部署文件夹的列表') .option('-f --force', 'force') .option('-i, --install ', 'install dependencies') .action(async (id, opts) => { let { force, key, install } = opts || {}; if (!key) { const answers = await inquirer.prompt([ { type: 'input', name: 'key', message: 'Enter your deploy to services fileKey:', when: () => !key, // 当 username 为空时,提示用户输入 }, ]); key = answers.key || key; } const res = await deployLoadFn(id, key, force, 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', } type ServiceItem = { key: string; status: 'inactive' | 'running' | 'stop' | 'error'; type: AppType; description: string; version: string; }; const servicesCommand = new Command('services') .description('服务器registry当中的服务管理') .option('-l, --list', 'list services') .option('-r, --restart ', 'restart services') .option('-s, --start ', 'start services') .option('-t, --stop ', 'stop services') .option('-i, --info ', 'info services') .option('-d, --delete ', '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);