sync: add d8d-weapp-publish skill and uni-app-compatibility rule #68

Merged
d8dfun merged 1 commits from sync/d8d-skills-20260507-143140 into master 2026-05-07 14:38:35 +08:00
7 changed files with 989 additions and 0 deletions

View File

@@ -0,0 +1,207 @@
# uni-app 兼容性检查铁律
## 规则 1: 自定义导航栏胶囊检测
**问题模式**: 扫描所有 `Navigation*.vue` / `NavBar*.vue` / `CustomHeader*.vue` 组件
**检查项**:
1.**生命周期正确性**: 必须使用 `onReady` (uni-app),禁止使用 `onMounted`
2.**跨平台检测**: 必须包含 `systemInfo.platform === 'h5'` 环境判断
3.**胶囊 API 调用**: 必须调用 `uni.getMenuButtonBoundingClientRect()`
4.**动态 padding**: 必须根据胶囊 `left` 位置计算 `padding-right`
5.**动态高度**: 必须根据胶囊 `bottom` 位置计算导航栏高度
**自动修复模板(检测到缺失时应用)**:
```typescript
import { ref } from "vue"
import { onReady } from "@dcloudio/uni-app"
const navBarHeight = ref(64)
const statusBarHeight = ref(0)
const rightPadding = ref(24)
onReady(() => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
// H5 环境下跳过胶囊适配
if (systemInfo.platform === "h5") return
// 微信小程序环境执行胶囊适配
const menuButton = uni.getMenuButtonBoundingClientRect()
// 计算导航栏总高度 = 胶囊底部 + 胶囊与状态栏间距
navBarHeight.value = menuButton.bottom + (menuButton.top - statusBarHeight.value)
// ⚠️ 重要:胶囊只在右侧,只需要给右侧加 padding左侧固定 24px
// 左侧不要动态计算,否则整个内容会被推到右边!
rightPadding.value = systemInfo.windowWidth - menuButton.left + 12
})
```
**模板内联样式绑定(含 Flex 防挤压)**:
```vue
<view
class="nav-bar"
:style="{
height: `${navBarHeight}px`,
paddingTop: `${statusBarHeight}px`,
paddingRight: `${rightPadding}px`,
paddingLeft: '24px',
}"
>
<view class="nav-left">
<text class="logo-text" style="white-space: nowrap">{{ logoText }}</text>
</view>
<view class="nav-right" style="flex-shrink: 0">
<!-- 图标 -->
</view>
</view>
```
**配套 CSS 规范(必须同时应用)**:
```css
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.nav-left {
flex: 1; /* ⚠️ 关键:左侧占据剩余空间,防止被右侧挤压 */
}
.nav-right {
flex-shrink: 0; /* ⚠️ 关键:右侧图标不被挤压 */
display: flex;
align-items: center;
gap: 20px;
}
.logo-text {
white-space: nowrap; /* ⚠️ 关键:标题不换行竖排 */
}
```
**违规后果**:
- 微信小程序环境下右侧图标被胶囊按钮遮挡
- 用户无法点击搜索、通知等核心功能按钮
- P0 级 UI 缺陷
---
## 规则 2: uni-app 生命周期误用检测
**检查范围**: 所有 Vue 组件文件
**禁止模式**:
- `onMounted` 中调用 `uni.*` API 进行布局计算
- `onMounted` 中调用 `uni.createSelectorQuery()`
- `onMounted` 中获取元素位置尺寸
**推荐模式**: 使用 `onReady` 替代uni-app 页面生命周期)
**自动修复**:
1.`onMounted``onReady`
2. 添加导入: `import { onReady } from '@dcloudio/uni-app'`
3. 移除错误的 `onMounted` 导入
**检测正则**:
```
/onMounted\s*\(\s*\(\s*\)\s*=>\s*{[\s\S]*?uni\.(getSystemInfoSync|getMenuButtonBoundingClientRect|createSelectorQuery)/
```
---
## 规则 3: pages.json 自定义导航栏自动检查
**触发条件**: 检测到 `"navigationStyle": "custom"` 配置
**自动执行**:
1. 扫描所有使用自定义导航栏的页面
2. 检查每个页面引用的导航栏组件是否通过胶囊适配检测
3. 为未通过检测的页面创建修复任务
**报告格式**:
```
⚠️ 自定义导航栏胶囊适配检测报告
--------------------------------
页面 pages/home/index: ✅ 已适配
页面 pages/square/index: ✅ 已适配
页面 pages/profile/index: ✅ 已适配
页面 pages/event-detail: ❌ 未适配
- 缺失: uni.getMenuButtonBoundingClientRect() 调用
- 建议: 应用规则 1 自动修复模板
```
---
## 规则 4: H5/MP 条件编译检查
**场景**: 小程序专属 API 不应该在 H5 环境下直接调用
**检测项**:
- ❌ 直接调用 `uni.getMenuButtonBoundingClientRect()` 而没有平台检测
- ❌ 直接调用 `wx.*` API 而没有条件编译包裹
- ✅ 推荐: 先检测 platform再执行平台专属代码
**安全模式**:
```typescript
// ✅ 正确 - 先检测平台
const systemInfo = uni.getSystemInfoSync()
if (systemInfo.platform !== "h5") {
const menuButton = uni.getMenuButtonBoundingClientRect()
// ...
}
```
**违规自动修复**: 为缺失平台检测的 API 调用添加 `if` 包裹
---
## 规则 5: 导航栏 Flex 布局防挤压(新增)
**问题描述**:左右 padding 很大时,如果没有 flex 布局控制,标题会被挤压得竖排显示,或者整体内容右移。
**检查项**
1.`.nav-left` 必须有 `flex: 1`
2.`.nav-right` 必须有 `flex-shrink: 0`
3. ✅ 标题文字必须有 `white-space: nowrap`
**修复优先级**P0用户直接感知的视觉缺陷
---
## 规则 6: wxss 标签名选择器禁用(新增)
**问题描述**:微信小程序组件 wxss 不允许使用标签名选择器(如 `svg`, `view`, `text`),编译后样式不生效,控制台警告。
**禁止的选择器模式**
```css
/* ❌ 禁止:标签名选择器 */
.icon-item > view svg { ... }
.nav-left text { ... }
view { ... }
```
**✅ 正确写法**
```css
/* 只使用类名选择器 */
.icon-item > .icon-wrapper { ... }
.nav-left .title { ... }
```
**检测命令**
```bash
grep -rn ">\s*svg\|>\s*view\|>\s*text" .claude/skills/d8d-dev/ app/mini-program/src/ --include="*.vue" --include="*.md"
```
---
## 规则 7: uni-app vs Vue 生命周期正确选择(新增)
| 使用场景 | 正确生命周期 | 错误选择 | 原因 |
|---------|-------------|---------|------|
| 获取胶囊位置、元素尺寸 | `onReady` | `onMounted` | `onMounted` 时组件可能还没渲染到位 |
| 获取 API 数据(普通场景) | `onMounted``onReady` | - | 都可以,通常 onMounted 足够 |
| navigateBack 返回后需要刷新数据 | `onShow` | `onMounted` | `onMounted` 只在首次创建时触发 |
**记忆口诀**:布局计算用 `onReady`,数据请求随便,返回刷新用 `onShow`

View File

@@ -0,0 +1,89 @@
---
name: d8d-weapp-publish
description: 微信小程序自动化发布工具。支持企业小程序和人才小程序的开发版和体验版发布。当用户需要:发布小程序到微信平台、上传体验版、生成开发版预览二维码、配置小程序发布环境时触发。使用 scripts/publish-weapp.js 脚本执行发布流程。
---
# 微信小程序发布
支持两种框架:**Taro**(企业小程序、人才小程序)和 **uni-app**(酒吧组局小程序)。
- **企业小程序**(用人方)- Taro
- **人才小程序**(求职者)- Taro
- **酒吧组局小程序** - uni-app
---
## 快速开始
### 发布命令
```bash
# 企业小程序 - 体验版上传
pnpm run publish:enterprise
# 企业小程序 - 开发版生成二维码
pnpm run publish:enterprise:dev
# 人才小程序 - 体验版上传
pnpm run publish:talent
# 人才小程序 - 开发版生成二维码
pnpm run publish:talent:dev
# 酒吧组局小程序 (uni-app) - 体验版上传
pnpm run publish:hazy
# 酒吧组局小程序 (uni-app) - 开发版生成二维码
pnpm run publish:hazy:dev
```
### 自定义版本和描述
```bash
VERSION=1.2.0 DESC="新增功能" pnpm run publish:enterprise
```
---
## 发布流程
### 自动构建说明
**重要**: 开发版预览会**自动重新构建** `development` 环境,确保使用最新代码。
**为什么开发版需要重新构建?**
微信小程序开发版预览有 **2MB 大小限制**,而使用 `pnpm run dev:weapp`watch 模式)生成的开发版输出体积较大(通常超过 2MB会导致上传失败
```
errcode: 80051, errmsg: source size 2368KB exceed max limit 2MB
```
因此开发版预览必须使用 `NODE_ENV=development` 进行完整构建,这样可以:
- 控制输出大小在限制内(约 1.3MB
- 确保使用最新代码
- 避免因 watch 模式输出过大导致上传失败
**构建环境说明**
- **开发版预览**: 使用 `NODE_ENV=development` 构建,输出到 `dist/weapp/development/`
- **体验版**: 使用 `NODE_ENV=production` 构建,输出到 `dist/weapp/production/`
如需跳过构建(使用已有构建输出),可添加 `--no-build` 参数:
```bash
node .claude/skills/d8d-weapp-publish/scripts/publish-weapp.js enterprise dev --no-build
```
### 发布步骤
1. 构建项目Taro 编译)
2. 上传到微信服务器
3. 生成体验版或预览二维码
---
## 参考文档
- **配置说明**: [config.md](references/config.md) - 小程序信息、package.json 配置、环境变量
- **常见问题**: [faq.md](references/faq.md) - 故障排查、注意事项
- **使用示例**: [usage.md](references/usage.md) - 完整使用示例、快捷命令

View File

@@ -0,0 +1,9 @@
{
"name": "weapp-publish-skill",
"version": "1.0.0",
"description": "WeChat mini-program publishing skill",
"type": "commonjs",
"dependencies": {
"miniprogram-ci": "^2.1.26"
}
}

View File

@@ -0,0 +1,185 @@
# 配置参考
## 支持的小程序
| 小程序 | 目录 | AppID | 说明 |
|--------|------|-------|------|
| **企业小程序** | `mini/` | `wx1e791ed2e0229eb8` | 用人方小程序(企业管理端) |
| **人才小程序** | `mini-talent/` | `wx3c47dbce1ea7d43c` | 人才小程序(求职者端) |
---
## 技能架构
```
.claude/skills/weapp-publish/
├── package.json # 技能依赖
├── node_modules/ # 技能依赖安装目录
├── SKILL.md # 技能说明
├── scripts/
│ └── publish-weapp.js # 发布脚本
└── references/ # 参考文档
├── config.md
├── faq.md
└── usage.md
```
**技能是自包含的**,有自己的依赖管理。
---
## 安装技能依赖
首次使用或更新依赖时,在技能目录安装:
```bash
cd .claude/skills/weapp-publish
pnpm install
```
---
## package.json 配置
项目根目录 `package.json` 中的发布命令:
```json
{
"scripts": {
"publish:enterprise": "node .claude/skills/weapp-publish/scripts/publish-weapp.js enterprise experience",
"publish:enterprise:dev": "node .claude/skills/weapp-publish/scripts/publish-weapp.js enterprise dev",
"publish:talent": "node .claude/skills/weapp-publish/scripts/publish-weapp.js talent experience",
"publish:talent:dev": "node .claude/skills/weapp-publish/scripts/publish-weapp.js talent dev"
}
}
```
---
## 命令行参数
### 发布类型 (action)
| 参数 | 说明 | 构建行为 | 输出路径 |
|------|------|----------|----------|
| `experience` | 体验版(默认) | 默认执行构建 | `dist/weapp/production/` |
| `dev` | 开发版 | 默认跳过构建 | `dist/weapp/development/` |
### 构建控制参数
| 参数 | 说明 |
|------|------|
| `--build` | 强制执行构建 |
| `--no-build` | 跳过构建 |
### 使用示例
```bash
# 开发版使用 watch 模式输出(快速)
node scripts/publish-weapp.js enterprise dev
# 开发版强制重新构建
node scripts/publish-weapp.js enterprise dev --build
# 体验版跳过构建(不推荐)
node scripts/publish-weapp.js enterprise experience --no-build
```
---
## 环境变量配置
发布时可通过环境变量自定义:
```bash
# 发布版本号
VERSION=1.2.0
# 发布描述
DESC=新增订单管理功能
# CI 机器人编号 (1-30)
ROBOT=1
# 公网访问域名(可选,用于预览二维码 URL
WEB_HOST=example.com
```
使用方式:
```bash
VERSION=1.2.0 DESC="新增功能" pnpm run publish:enterprise
```
---
## 输出路径说明
发布脚本根据类型自动选择输出路径:
| 类型 | 环境变量 | 输出目录 | 用途 |
|------|----------|----------|------|
| **体验版** | `NODE_ENV=production` | `dist/weapp/production/` | 提交审核/体验版 |
| **开发版** | `NODE_ENV=development` | `dist/weapp/development/` | 开发调试 |
开发版使用 `.env.development` 中的 API 配置,体验版使用 `.env.production`
---
## 发布前准备
### 1. 安装技能依赖
```bash
cd .claude/skills/weapp-publish
pnpm install
```
### 2. 获取上传密钥
使用小程序管理员身份访问"微信公众平台 > 开发 > 开发设置",下载代码上传密钥。
**企业小程序密钥文件**: `mini/certs/private.upload.key`
**人才小程序密钥文件**: `mini-talent/certs/private.upload.key`
> 密钥文件已存在于各自小程序目录的 `certs/` 目录下。
### 3. 配置 IP 白名单
在微信公众平台配置服务器的 IP 白名单。
### 4. 确保 ES5 转译配置
小程序的 `config/dev.ts` 中应包含以下配置,确保所有依赖被转译为 ES5
```ts
mini: {
compile: {
include: [
// 确保产物为 es5转译所有 node_modules除了 Babel 核心)
filename => /node_modules\/(?!(@babel|core-js|style-loader|css-loader|react|react-dom))/.test(filename)
]
}
}
```
**原因**:微信小程序的预览编译器不完全支持 ES6+ 语法(如可选链 `?.`),需要确保所有代码被转译为 ES5。
### 5. 开发版预览 - 关闭 Prebundle重要
微信小程序开发版预览有 **2MB 主包大小限制**Taro 在开发模式默认开启 prebundle依赖预编译会导致主包超过限制。
`config/dev.ts` 中添加以下配置关闭 prebundle
```ts
compiler: {
// 关闭开发模式的依赖预编译减少主包体积以支持微信小程序开发版预览2MB限制
prebundle: {
enable: false
}
}
```
**注意**
- 此配置仅影响开发版预览,不影响体验版和生产版
- 关闭 prebundle 后首次编译会稍慢,但主包体积可从 ~3MB 降至 ~650KB
- 人才小程序 (`mini-talent`) 也需要同样配置

View File

@@ -0,0 +1,100 @@
# 常见问题
### Q: 发布失败提示 IP 不在白名单
**A**: 请在微信公众平台"开发 > 开发设置"中添加服务器 IP 到白名单。
### Q: 构建成功但发布失败
**A**: 检查密钥文件路径是否正确,密钥是否匹配对应的 AppID。
### Q: 构建时提示找不到模块(如 `tailwindcss` 或 `@/` 路径别名)
**A**: **此问题已在脚本中修复**2025-03-18。如果仍然遇到请使用 `--no-build` 参数作为备用方案。
**备用方案**
```bash
# 1. 进入小程序目录手动构建
cd mini
NODE_ENV=production pnpm taro build --type weapp
# 2. 返回根目录,使用 --no-build 发布
cd ..
pnpm run publish:enterprise --no-build
```
### Q: 如何设置体验版
**A**: 发布成功后,登录小程序后台"版本管理",将指定版本设置为体验版。
### Q: 开发版预览二维码生成失败,提示语法错误
**A**: 这通常是因为代码中包含了微信小程序不支持的 ES6+ 语法(如可选链 `?.`)。
**解决方案**
1. 确保小程序的 `config/dev.ts` 中配置了 ES5 转译:
```ts
mini: {
compile: {
include: [
filename => /node_modules\/(?!(@babel|core-js|style-loader|css-loader|react|react-dom))/.test(filename)
]
}
}
```
2. 使用 `--build` 参数强制重新构建
### Q: 开发版为什么默认跳过构建?
**A**: 开发版默认使用 `dev:weapp` (watch 模式) 的构建输出,因为:
1. 开发时通常会运行 watch 模式,代码实时编译
2. 跳过构建可以快速生成预览二维码,提高开发效率
3. 如需重新构建,添加 `--build` 参数
### Q: 体验版和开发版有什么区别?
**A**:
| 类型 | 用途 | 构建行为 | 输出路径 |
|------|------|----------|----------|
| **体验版** (experience) | 提交给测试人员体验 | 默认执行构建 | `dist/weapp/production/` |
| **开发版** (dev) | 开发者自测 | 默认跳过构建 | `dist/weapp/development/` |
### Q: 开发版预览上传失败,提示超过 2MB 限制
**A**: 微信小程序开发版预览有严格的 **2MB 主包大小限制**,这是微信服务器端的硬性限制,无法通过配置参数绕过。
**根本原因**
1. `maxUploadSourceSize` **不是** miniprogram-ci 的有效参数(只在 `project.config.json` 中对本地开发者工具有效)
2. Taro 在开发模式默认开启 **prebundle依赖预编译**,预编译的依赖包会占用约 2MB 空间
**解决方案**:关闭开发模式的 prebundle 功能
在 `mini/config/dev.ts` 中添加:
```typescript
compiler: {
// 关闭开发模式的依赖预编译减少主包体积以支持微信小程序开发版预览2MB限制
prebundle: {
enable: false
}
}
```
**优化效果**
| 优化前 | 优化后 | 减少 |
|--------|--------|------|
| ~3MB | ~650KB | ~78% |
**注意**
- 关闭 prebundle 后首次编译会稍慢(需要完整编译依赖)
- 不影响体验版和生产版(这些版本本身就需要完整的依赖编译)
---
## 注意事项
1. **密钥安全**: 上传密钥拥有预览、上传权限,请勿提交到代码仓库
2. **构建依赖**: 发布前确保项目依赖已安装 (`pnpm install`)
3. **版本管理**: 建议使用语义化版本号 (Semantic Versioning)
4. **IP 白名单**: 确保服务器 IP 已在微信公众平台配置白名单
5. **机器人编号**: 多个 CI 并发时使用不同的 robot 编号 (1-30)
6. **开发模式**: 开发版预览前请确保 `dev:weapp` 正在运行

View File

@@ -0,0 +1,69 @@
# 使用示例
## 发布企业小程序
```bash
# 使用默认版本号和描述(体验版)
pnpm run publish:enterprise
# 自定义版本号和描述
VERSION=1.2.0 DESC="新增订单管理功能" pnpm run publish:enterprise
# 生成预览二维码(默认跳过构建,使用 watch 模式输出)
pnpm run publish:enterprise:dev
# 强制重新构建后生成预览二维码
pnpm run publish:enterprise:dev --build
```
## 发布人才小程序
```bash
# 使用默认版本号和描述(体验版)
pnpm run publish:talent
# 自定义版本号和描述
VERSION=2.0.0 DESC="全新改版,优化求职流程" pnpm run publish:talent
# 生成预览二维码(默认跳过构建,使用 watch 模式输出)
pnpm run publish:talent:dev
# 强制重新构建后生成预览二维码
pnpm run publish:talent:dev --build
```
---
## 构建行为说明
### 开发版 (dev)
- **默认跳过构建**:直接使用 `dev:weapp` (watch 模式) 的输出
- 适用场景:本地开发时快速生成预览二维码
- 强制构建:添加 `--build` 参数
### 体验版 (experience)
- **默认执行构建**:确保发布的是最新代码
- 跳过构建:添加 `--no-build` 参数(适用于已手动构建的情况)
### 输出路径
- 开发版:`dist/weapp/development/`
- 体验版:`dist/weapp/production/`
---
## 快捷命令参考
| 用户指令 | Claude 行为 |
|----------|-------------|
| "发布企业小程序" | 运行 `pnpm run publish:enterprise` |
| "发布人才小程序" | 运行 `pnpm run publish:talent` |
| "生成企业小程序预览二维码" | 运行 `pnpm run publish:enterprise:dev` |
| "发布两个小程序" | 依次运行企业小程序和人才小程序的发布命令 |
---
## 相关资源
- [miniprogram-ci 官方文档](https://www.npmjs.com/package/miniprogram-ci)
- [微信公众平台](https://mp.weixin.qq.com/)
- [miniprogram-ci Skill](.claude/skills/miniprogram-ci/SKILL.md)

View File

@@ -0,0 +1,330 @@
const ci = require('miniprogram-ci')
const path = require('path')
const { execSync } = require('child_process')
const fs = require('fs')
// 公网访问域名(从环境变量读取,默认本地)
const WEB_HOST = process.env.WEB_HOST || 'localhost:8080'
const PUBLIC_HOST = WEB_HOST.startsWith('http') ? WEB_HOST : `https://${WEB_HOST}`
// 项目根目录(从 scripts/ 目录向上 4 级)
const PROJECT_ROOT = path.resolve(__dirname, '../../../../')
// 小程序配置基础信息
const MINI_CONFIGS = {
enterprise: {
name: '企业小程序',
framework: 'taro',
appid: 'wx1e791ed2e0229eb8',
dir: 'mini',
privateKeyPath: path.resolve(PROJECT_ROOT, 'mini/certs/private.upload.key'),
distPath: 'dist/weapp',
},
talent: {
name: '人才小程序',
framework: 'taro',
appid: 'wx3c47dbce1ea7d43c',
dir: 'mini-talent',
privateKeyPath: path.resolve(PROJECT_ROOT, 'mini-talent/certs/private.upload.key'),
distPath: 'dist/weapp',
},
hazy: {
name: '酒吧组局',
framework: 'uniapp',
appid: process.env.WECHAT_APPID || '',
dir: 'app/mini-program',
privateKeyPath: path.resolve(PROJECT_ROOT, 'app/mini-program/certs/private.upload.key'),
distPath: 'dist/build/mp-weixin',
},
}
/**
* 获取构建配置(根据 action 类型动态选择)
*
* 说明:
* - dev (开发版预览): 使用 development 模式,自动构建
* - experience (体验版): 使用 production 模式,自动构建(体验版=生产环境)
* - production (正式版): 使用 production 模式,自动构建
*
* 目录结构dist/weapp/{mode}/
*/
function getBuildConfig(config, action) {
// 只有开发版使用 development 模式
// 体验版和正式版都使用 production 模式
const mode = action === 'dev' ? 'development' : 'production'
// 根据模式设置对应的 NODE_ENV
// 这样构建输出目录和上传目录就能匹配了
const nodeEnv = mode === 'development' ? 'development' : 'production'
// 小程序目录的绝对路径,用于 execSync 的 cwd 参数
const miniDir = path.resolve(PROJECT_ROOT, config.dir)
// 根据框架选择不同的构建命令和输出路径
let buildCmd, projectPath
if (config.framework === 'uniapp') {
// uni-app: 固定输出路径,不区分环境目录
buildCmd = `NODE_ENV=${nodeEnv} pnpm build:mp-weixin`
projectPath = path.resolve(PROJECT_ROOT, `${config.dir}/${config.distPath}`)
} else {
// Taro: 按环境分目录输出
buildCmd = `NODE_ENV=${nodeEnv} pnpm taro build --type weapp`
projectPath = path.resolve(PROJECT_ROOT, `${config.dir}/${config.distPath}/${mode}`)
}
return {
...config,
mode,
nodeEnv,
miniDir,
projectPath,
buildCmd,
}
}
/**
* 构建小程序
*/
function buildMiniProject(config) {
console.log(`\n🔨 正在构建 ${config.name}...`)
console.log(` 工作目录: ${config.miniDir}`)
console.log(` 环境变量: NODE_ENV=${config.nodeEnv}`)
try {
execSync(config.buildCmd, { stdio: 'inherit', cwd: config.miniDir, shell: true })
console.log(`${config.name} 构建完成`)
} catch (error) {
console.error(`${config.name} 构建失败`)
throw error
}
}
/**
* 发布小程序体验版到微信服务器
*/
async function uploadExperienceProject(config, options) {
const { version, desc, robot = 1 } = options
console.log(`\n📤 正在发布体验版 ${config.name}...`)
console.log(` 版本: ${version}`)
console.log(` 描述: ${desc}`)
// 更新 project.config.json 中的版本号(确保微信后台正确显示)
const projectConfigPath = path.join(config.projectPath, 'project.config.json')
if (fs.existsSync(projectConfigPath)) {
const projectConfig = JSON.parse(fs.readFileSync(projectConfigPath, 'utf-8'))
projectConfig.version = version
fs.writeFileSync(projectConfigPath, JSON.stringify(projectConfig, null, 2))
console.log(` 已更新 project.config.json 版本号为: ${version}`)
}
const project = new ci.Project({
appid: config.appid,
type: 'miniProgram',
projectPath: config.projectPath,
privateKeyPath: config.privateKeyPath,
ignores: ['node_modules/**/*'],
})
try {
const result = await ci.upload({
project,
version,
desc,
setting: {
useProjectConfig: true,
es7: true,
minify: true,
minifyJS: true,
minifyWXML: true,
minifyWXSS: true,
autoPrefixWXSS: true,
},
onProgressUpdate: (progress) => {
process.stdout.write(`\r 上传进度: ${progress}%`)
},
robot,
})
console.log(`\n${config.name}体验版上传成功!`)
console.log(` 时间: ${result.time}`)
console.log(` 版本: ${result.version}`)
console.log(` 请前往小程序后台查看: https://mp.weixin.qq.com/`)
} catch (error) {
console.error(`\n${config.name}体验版上传失败`)
console.error(error.message)
throw error
}
}
/**
* 预览小程序(生成二维码)
*/
async function previewDevProject(config, options) {
const { desc, robot = 1 } = options
console.log(`\n👀 正在生成 ${config.name} 预览二维码...`)
const project = new ci.Project({
appid: config.appid,
type: 'miniProgram',
projectPath: config.projectPath,
privateKeyPath: config.privateKeyPath,
ignores: ['node_modules/**/*'],
})
// 二维码保存目录
const qrcodeDir = path.resolve(PROJECT_ROOT, 'web/public/qrcode')
const qrcodeFileName = `qrcode-${config.appid}.jpg`
const qrcodePath = path.join(qrcodeDir, qrcodeFileName)
// 确保 qrcode 目录存在
if (!fs.existsSync(qrcodeDir)) {
fs.mkdirSync(qrcodeDir, { recursive: true })
}
try {
await ci.preview({
project,
desc,
setting: {
useProjectConfig: false, // 不使用项目配置,完全使用下方自定义设置
es6: false,
es7: false, // 增强编译
minify: true, // 压缩所有代码
minifyWXSS: true, // 压缩样式
minifyWXML: true, // 压缩模板
minifyJS: true, // 压缩JS
codeProtect: false,
autoPrefixWXSS: false,
},
qrcodeFormat: 'image',
qrcodeOutputDest: qrcodePath,
onProgressUpdate: (progress) => {
process.stdout.write(`\r 生成进度: ${progress}%`)
},
robot,
uploadType: 'preview',
})
// 公网访问 URL
const publicUrl = `${PUBLIC_HOST}/qrcode/${qrcodeFileName}`
console.log(`\n✅ 开发版预览二维码已生成!`)
console.log(`📱 本地路径: ${qrcodePath}`)
console.log(`🌐 公网访问: ${publicUrl}`)
console.log(`\n>>>QRCode_IMAGE_START<<<`)
console.log(qrcodePath)
console.log(`>>>QRCode_URL_START<<<`)
console.log(publicUrl)
console.log(`>>>QRCode_URL_END<<<`)
} catch (error) {
console.error(`\n❌ 开发版预览二维码生成失败`)
console.error(error.message)
throw error
}
}
/**
* 递增版本号 (例如: 0.0.39 -> 0.0.40)
*/
function bumpVersion(version) {
const parts = version.split('.')
const patch = parseInt(parts[2] || '0')
parts[2] = (patch + 1).toString()
return parts.join('.')
}
/**
* 从 package.json 读取当前版本号
*/
function getCurrentVersion(config) {
const packageJsonPath = path.join(PROJECT_ROOT, config.dir, 'package.json')
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
return packageJson.version || '1.0.0'
}
return '1.0.0'
}
/**
* 更新 package.json 中的版本号
*/
function updatePackageVersion(config, version) {
const packageJsonPath = path.join(PROJECT_ROOT, config.dir, 'package.json')
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
packageJson.version = version
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
console.log(` 已更新 package.json 版本号为: ${version}`)
}
}
/**
* 主函数
*/
async function main() {
const args = process.argv.slice(2)
// --no-build 参数可以跳过构建(默认会自动构建)
const noBuildIndex = args.indexOf('--no-build')
const noBuild = noBuildIndex !== -1
if (noBuild) {
args.splice(noBuildIndex, 1)
}
const [miniType, action = 'experience'] = args
// 只有指定 --no-build 时才跳过构建
const shouldSkipBuild = noBuild
// 获取构建配置(需要提前获取以便读取版本号)
const config = getBuildConfig(MINI_CONFIGS[miniType], action)
// 解析参数 - 体验版如果没有指定 VERSION则自动递增版本号
let version = process.env.VERSION
if (action === 'experience' && !version) {
const currentVersion = getCurrentVersion(config)
version = bumpVersion(currentVersion)
console.log(`\n🔢 自动递增版本号: ${currentVersion} -> ${version}`)
updatePackageVersion(config, version)
}
const options = {
version: version || '1.0.0',
desc: process.env.DESC || '自动化发布',
robot: parseInt(process.env.ROBOT || '1'),
}
if (!miniType || !MINI_CONFIGS[miniType]) {
console.error('❌ 请指定小程序类型: enterprise 或 talent')
console.error('示例: node scripts/publish-weapp.js enterprise')
process.exit(1)
}
try {
// 构建项目(跳过或显式指定构建时)
if (!shouldSkipBuild && !noBuild) {
buildMiniProject(config)
} else {
console.log(`\n⏭️ 跳过构建,使用现有构建输出`)
console.log(` 使用路径: ${config.projectPath}`)
}
// 执行操作
if (action === 'experience') {
await uploadExperienceProject(config, options)
} else if (action === 'dev') {
await previewDevProject(config, {
desc: options.desc,
robot: options.robot,
})
} else {
console.error(`❌ 操作无效: ${action}`)
process.exit(1)
}
} catch (_error) {
process.exit(1)
}
}
main()