npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jl-org/deploy

v1.0.8

Published

一键部署项目到服务器

Readme

🚀 Deploy

一个基于 SSH 的自动化部署工具,支持多服务器部署、文件备份、交互式部署和自定义部署流程

✨ 特性

  • 🔨 自动构建、压缩、上传和部署
  • 🌐 支持多服务器并行部署
  • 🤝 交互式部署模式:在每个阶段询问用户确认,提供精细控制
  • 💾 支持远程文件备份和清理
  • 🎨 友好的彩色日志输出
  • ⚙️ 自定义上传和部署行为
  • ⚡ 可跳过构建步骤,提高部署效率
  • 🎣 丰富的生命周期 Hooks:在每个部署阶段前后执行自定义逻辑
  • ⚠️ 结构化错误处理:统一的错误码和错误类型,便于 CI/CD 集成

demo


📦 安装

npm install @jl-org/deploy
# 或
yarn add @jl-org/deploy
# 或
pnpm add @jl-org/deploy

🚀 基本用法

代码示例

scripts/deploy.cjs

// @ts-check
const { deploy } = require('@jl-org/deploy')
const { resolve } = require('node:path')
const { homedir } = require('node:os')
const { readFileSync } = require('node:fs')

deploy({
  // ======================
  // 🔗 SSH 连接信息
  // ======================
  connectInfos: [
    {
      name: 'server-1', // 服务器名称(可选,用于日志显示)
      host: '192.168.1.100',
      port: 22,
      username: 'root',
      password: 'password',
      // 如果使用私钥登录,不使用密码登录的话
      // privateKey: readFileSync(resolve(homedir(), '.ssh/id_rsa'), 'utf-8'),
    }
  ],
  
  // ======================
  // 🔨 本地构建配置
  // ======================
  buildCmd: 'npm run build', // 构建命令
  distDir: resolve(__dirname, '../dist'), // 构建产物目录
  skipBuild: false, // 是否跳过构建步骤
  
  // 📦 压缩文件配置
  zipPath: resolve(__dirname, '../dist.tar.gz'), // 本地压缩文件路径
  
  // ======================
  // 🌐 远程服务器配置
  // ======================
  remoteZipPath: '/home/dist.tar.gz', // 远程压缩文件路径
  remoteUnzipDir: '/home/test-project', // 远程解压目录
  remoteCwd: '/', // 远程命令执行路径
  deployCmd: '', // (可选)远程服务器部署命令,已有默认值,建议不要改动。和 customDeploy 回调冲突
  
  // ======================
  // 💾 备份配置(可选)
  // ======================
  remoteBackupDir: '/home/test-project-backup', // 远程备份目录
  maxBackupCount: 5, // 保留最近的备份数量
  
  // ======================
  // ⚙️ 其他选项
  // ======================
  needRemoveZip: true, // 是否需要删除本地压缩文件
  uploadRetryCount: 3, // 上传失败重试次数
  interactive: false, // 禁用交互模式(默认)
  concurrent: true // 并发部署(默认)
})

🤝 交互式部署模式

启用交互模式后,系统将在每个阶段前询问:

  • 🔨 构建阶段:是否执行构建命令
  • 📦 压缩阶段:是否压缩构建产物
  • 🚀 上传和部署阶段:是否上传并部署到服务器
  • 🧹 清理阶段:是否清理本地临时文件

🎯 高级用法

📤 自定义上传行为

deploy({
  // ...基本配置
  
  // 自定义上传行为
  customUpload: async (createServer) => {
    const server = createServer()
    // 自定义连接和上传逻辑
    return [server]
  }
})

🎛️ 自定义部署行为

deploy({
  // ...基本配置
  
  // 自定义部署行为
  customDeploy: async (servers, connectInfos) => {
    // 自定义解压和部署逻辑
    for (const server of servers) {
      // 执行自定义部署命令
    }
  }
})

🔧 服务器就绪回调

deploy({
  // ...基本配置
  
  // 服务器连接成功回调
  onServerReady: async (server, connectInfo) => {
    // 服务器连接成功后,部署前的自定义操作
  }
})

🎣 生命周期 Hooks

deploy({
  // ...基本配置
  
  // 🔨 构建阶段 hooks
  onBeforeBuild: async (context) => {
    console.log('准备开始构建...', context.buildCmd)
  },
  onAfterBuild: async (context) => {
    console.log('构建完成!')
  },
  
  // 📦 压缩阶段 hooks
  onBeforeCompress: async (context) => {
    console.log('开始压缩文件...', context.distDir)
  },
  onAfterCompress: async (context) => {
    console.log('压缩完成!', context.zipPath)
  },
  
  // 🔗 连接阶段 hooks
  onBeforeConnect: async (context) => {
    console.log('准备连接服务器...', context.connectInfos.length)
  },
  onAfterConnect: async (context) => {
    console.log('服务器连接成功!')
  },
  
  // 📤 上传阶段 hooks(每个服务器单独触发)
  onBeforeUpload: async (context) => {
    console.log('开始上传到:', context.connectInfo.host)
  },
  onAfterUpload: async (context) => {
    console.log('上传成功:', context.connectInfo.host)
  },
  
  // 🚀 部署阶段 hooks
  onBeforeDeploy: async (context) => {
    console.log('开始部署...', context.sshClients.length)
  },
  onAfterDeploy: async (context) => {
    console.log('部署完成!')
  },
  
  // 🧹 清理阶段 hooks
  onBeforeCleanup: async (context) => {
    console.log('准备清理临时文件...', context.zipPath)
  },
  onAfterCleanup: async (context) => {
    console.log('清理完成!')
  },
  
  // ❌ 全局错误处理 hook
  onError: async (context) => {
    console.error('部署出错:', context.error.code, context.error.message)
    
    // 发送错误通知,记录日志等
    await sendErrorNotification(context.error)
    
    // 返回 true 表示错误已处理,继续执行;返回 false 或不返回表示重新抛出错误
    return false
  }
})

⚠️ 错误处理

该工具提供了结构化的错误处理,便于 CI/CD 集成:

import { deploy, DeployError, DeployErrorCode } from '@jl-org/deploy'

try {
  await deploy({
    // 配置...
  })
} catch (error) {
  if (error instanceof DeployError) {
    console.error('部署错误码:', error.code)
    console.error('错误消息:', error.message)
    console.error('服务器:', error.serverName)
    console.error('详细信息:', error.details)
    
    // 根据错误码执行不同的处理逻辑
    switch (error.code) {
      case DeployErrorCode.BUILD_COMMAND_FAILED:
        // 构建失败处理
        break
      case DeployErrorCode.CONNECT_SSH_FAILED:
        // SSH 连接失败处理
        break
      case DeployErrorCode.UPLOAD_FILE_FAILED:
        // 文件上传失败处理
        break
      // ...
    }
  }
}

🔌 全自定义流程:组合底层 API

当需要完全自定义流程(例如只做压缩、只做上传,或自定执行顺序)时,可直接使用以下 API 自行组合,无需走完整 deploy 流程

| API | 说明 | | --- | --- | | sshRemote(connectInfo, task) | 建立 SSH 连接并执行回调。task(client) 内可使用 client.execclient.shell 等执行任意命令,返回 task 的返回值。 | | sftpRemote(connectInfo, task) | 建立 SFTP 连接并执行回调。task(sftp) 内可使用 sftp.fastPutfastGetreaddirmkdir 等完成文件上传/下载,返回 task 的返回值。 | | compress(options) | 将目录打成 tar.gz。options: { distDir, zipPath, onProgress? },返回 Promise<{ bytesWritten }>。无控制台日志,适合脚本或自定义流水线。 | | startZip(opts) | 与 compress 相同能力,但带控制台日志与进度条,适合人类可读的部署流程。opts: { distDir, zipPath }。 |

示例:仅压缩、或先压缩再通过 SFTP 上传:

import { compress, sftpRemote } from '@jl-org/deploy'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { homedir } from 'node:os'

// 仅压缩
const { bytesWritten } = await compress({
  distDir: resolve(__dirname, '../dist'),
  zipPath: resolve(__dirname, '../dist.tar.gz'),
  onProgress(processed, total) { console.log(processed, total) }
})

// 再通过 SFTP 上传到远程
await sftpRemote(
  { host: '192.168.1.100', username: 'root', privateKey: readFileSync(resolve(homedir(), '.ssh/id_rsa'), 'utf-8') },
  async (sftp) => {
    await new Promise((res, rej) => {
      sftp.fastPut(resolve(__dirname, '../dist.tar.gz'), '/home/dist.tar.gz', (err) => (err ? rej(err) : res()))
    })
  }
)

📋 配置选项

| 选项 | 类型 | 默认值 | 描述 | | --- | --- | --- | --- | | connectInfos | ConnectInfo[] | - | 🔗 必填,SSH 连接信息数组 | | buildCmd | string | 'npm run build' | 🔨 构建命令 | | skipBuild | boolean | false | ⚡ 是否跳过构建步骤 | | interactive | boolean | false | 🤝 是否启用交互式部署模式 | | concurrent | boolean | true | 🌐 是否并发部署到多个服务器 | | deployCmd | string | 见下方 | 🚀 远程服务器部署命令 | | distDir | string | - | 📁 必填,构建产物目录路径 | | zipPath | string | - | 📦 必填,压缩文件路径 | | remoteZipPath | string | - | 🌐 必填,远程压缩文件路径 | | remoteUnzipDir | string | - | 📁 必填,远程解压目录路径 | | remoteBackupDir | string | - | 💾 远程备份目录路径 | | maxBackupCount | number | 5 | 🔢 最大备份数量 | | remoteCwd | string | '/' | 📍 远程命令执行路径 | | needRemoveZip | boolean | true | 🗑️ 是否删除本地压缩文件 | | uploadRetryCount | number | 3 | 🔄 上传失败重试次数 | | onServerReady | function | - | 🔧 服务器准备完毕回调 | | customUpload | function | - | 📤 自定义上传行为 | | customDeploy | function | - | 🎛️ 自定义部署行为 |

🎣 生命周期 Hooks

| Hook | 类型 | 描述 | | --- | --- | --- | | onBeforeBuild | function | 🔨 构建阶段开始前的回调 | | onAfterBuild | function | ✅ 构建阶段完成后的回调 | | onBeforeCompress | function | 📦 压缩阶段开始前的回调 | | onAfterCompress | function | ✅ 压缩阶段完成后的回调 | | onBeforeConnect | function | 🔗 连接阶段开始前的回调 | | onAfterConnect | function | ✅ 连接阶段完成后的回调 | | onBeforeUpload | function | 📤 上传阶段开始前的回调(每个服务器单独触发) | | onAfterUpload | function | ✅ 上传阶段完成后的回调(每个服务器单独触发) | | onBeforeDeploy | function | 🚀 部署阶段开始前的回调 | | onAfterDeploy | function | ✅ 部署阶段完成后的回调 | | onBeforeCleanup | function | 🧹 清理阶段开始前的回调 | | onAfterCleanup | function | ✅ 清理阶段完成后的回调 | | onError | function | ❌ 全局错误处理回调 |

🔧 默认部署命令:

cd ${remoteCwd} &&
rm -rf ${remoteUnzipDir} &&
mkdir -p ${remoteUnzipDir} &&
tar -xzf ${remoteZipPath} -C ${remoteUnzipDir} &&
rm -rf ${remoteZipPath} &&
exit

🚨 错误码

常见的错误码包括:

  • CONFIG_VALIDATION_FAILED - 📋 配置验证失败
  • BUILD_COMMAND_FAILED - 🔨 构建命令执行失败
  • COMPRESS_SOURCE_NOT_FOUND - 📦 压缩源目录不存在
  • CONNECT_SSH_FAILED - 🔗 SSH 连接失败
  • UPLOAD_FILE_FAILED - 📤 文件上传失败
  • DEPLOY_COMMAND_FAILED - 🚀 部署命令执行失败
  • USER_CANCELLED - 🚫 用户取消操作

更多错误码请参考 DeployErrorCode 枚举


🐛 常见错误

Hook 中 shell.sftp 正确用法

各阶段 Hook 的 context.shell 提供 execspawnsftp 三种远程能力。sftp 基于 ssh2 的 SFTPWrapper,可调用 fastPutfastGetreaddirmkdirstat 等 API

❌ 错误写法fastPut / fastGet 等是回调式 API,直接调用而不 await 会导致 task 过早返回,SFTP 连接在上传/下载完成前即被关闭:

onAfterDeploy: async (context) => {
  const { shell } = context
  shell.sftp(async (sftp) => {
    sftp.fastPut(localPath, remotePath, (err) => {
      if (err) console.error(err)
    })
    // ⚠️ 此处 async 函数立即返回,连接被关闭,上传可能中断!
  })
}

✅ 正确写法:将回调式 API 包装为 Promise 并 await,确保传输完成后再关闭连接:

onAfterDeploy: async (context) => {
  const { shell } = context
  await shell.sftp(async (sftp) => {
    await new Promise((resolve, reject) => {
      sftp.fastPut(localPath, remotePath, (err) => {
        if (err) reject(err)
        else resolve()
      })
    })
  })
}

📌 要点shell.sftp(task) 每次调用会新建 SSH 连接,task 返回后自动关闭。task 内所有异步操作都需 await 完成后,连接才会保持到操作结束。


⚠️ 注意事项

  1. 📁 remoteUnzipDir 不应与 remoteZipPath 的目录相同,因为部署过程中会先删除 remoteUnzipDir 目录
  2. 📝 使用自定义 deployCmd 时,命令末尾必须有换行符
  3. skipBuild 为 true 时,会检查构建产物目录是否存在,不存在则报错
  4. 🤖 在 CI/CD 环境中,使用 interactive: false 以避免阻塞
  5. 🎣 使用 hooks 可以在部署流程的各个阶段执行自定义逻辑,便于集成监控、通知等功能
  6. ⚠️ 错误处理提供了结构化的错误信息,便于 CI/CD 系统根据错误码执行不同的处理策略