| .. | .. |
|---|
| 1 | 1 | /** |
|---|
| 2 | | - * HBuilderX CLI 命令行发布:按环境发布微信小程序 |
|---|
| 2 | + * 命令行发行(不上传)+ 自动用微信开发者工具打开产物 |
|---|
| 3 | 3 | * |
|---|
| 4 | 4 | * 用法: |
|---|
| 5 | | - * node scripts/publish.js sit # 编译 + 上传到 SIT 小程序(微信 CI,无需打开微信开发者工具) |
|---|
| 6 | | - * node scripts/publish.js prod # 编译 + 上传到 PROD 小程序 |
|---|
| 7 | | - * node scripts/publish.js sit --local # 只本地编译,不上传 |
|---|
| 8 | | - * node scripts/publish.js sit --dry-run # 只打印将要执行的命令 |
|---|
| 9 | | - * node scripts/publish.js sit --version 2.0.0 --robot 3 # 额外参数原样透传给 cli |
|---|
| 5 | + * node scripts/publish.js sit # 发行 SIT 版本并打开微信开发者工具 |
|---|
| 6 | + * node scripts/publish.js prod # 发行 PROD 版本并打开微信开发者工具 |
|---|
| 7 | + * node scripts/publish.js sit --sync # 同时把 appid 同步进 manifest.json(默认不动 manifest) |
|---|
| 8 | + * node scripts/publish.js sit --no-open # 只编译,不打开微信开发者工具 |
|---|
| 9 | + * node scripts/publish.js sit --dry-run # 只打印将要执行的命令 |
|---|
| 10 | 10 | * |
|---|
| 11 | | - * 环境相关的值统一取自 config/env/<env>.js(唯一数据源): |
|---|
| 12 | | - * - 接口/埋点等运行时参数:包内自动按小程序 appid 识别,无需在此处理 |
|---|
| 13 | | - * - APPID:作为 --appid 传给微信 CI 上传 |
|---|
| 11 | + * 环境值统一取自 config/env/<env>.js(唯一数据源),接口地址等运行时参数由包内自动识别, |
|---|
| 12 | + * 本脚本只负责让“编译期 appid”与目标环境一致 —— 微信开发者工具打开的产物 appid, |
|---|
| 13 | + * 就是 unpackage/dist/build/mp-weixin/project.config.json 里的 appid(构建时由 manifest 生成)。 |
|---|
| 14 | 14 | * |
|---|
| 15 | | - * 密钥文件:keys/private.<env>.key |
|---|
| 16 | | - * 获取方式:微信公众平台 → 开发管理 → 开发设置 → 小程序代码上传 → 生成上传密钥 |
|---|
| 17 | | - * 注意:密钥属于机密,已通过 .gitignore 排除,切勿提交 |
|---|
| 15 | + * 默认策略:不动 manifest.json(仓库保持 prod 基线,git 无噪音),只回写未跟踪的产物文件。 |
|---|
| 18 | 16 | */ |
|---|
| 19 | 17 | const { spawnSync } = require('child_process'); |
|---|
| 20 | 18 | const fs = require('fs'); |
|---|
| .. | .. |
|---|
| 22 | 20 | |
|---|
| 23 | 21 | const ROOT = path.resolve(__dirname, '..'); |
|---|
| 24 | 22 | const ENV_DIR = path.join(ROOT, 'config', 'env'); |
|---|
| 23 | +const DIST_DIR = path.join(ROOT, 'unpackage', 'dist', 'build', 'mp-weixin'); |
|---|
| 24 | +const DIST_CONFIG = path.join(DIST_DIR, 'project.config.json'); |
|---|
| 25 | 25 | |
|---|
| 26 | | -// HBuilderX cli.exe 路径:优先读环境变量,便于换机器 / 换安装位置 |
|---|
| 26 | +// 外部工具路径:可用环境变量覆盖,便于换机器 |
|---|
| 27 | 27 | const HX_CLI = process.env.HBUILDERX_CLI || 'C:\\Users\\Administrator\\Desktop\\HBuilderX\\cli.exe'; |
|---|
| 28 | +const WX_CLI = process.env.WX_DEVTOOLS_CLI || 'D:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat'; |
|---|
| 28 | 29 | |
|---|
| 29 | 30 | const argv = process.argv.slice(2); |
|---|
| 30 | 31 | const envName = (argv.find(function(a) { return a.charAt(0) !== '-'; }) || '').trim(); |
|---|
| 31 | | -const localOnly = argv.indexOf('--local') > -1; |
|---|
| 32 | +const syncManifest = argv.indexOf('--sync') > -1; |
|---|
| 33 | +const noOpen = argv.indexOf('--no-open') > -1; |
|---|
| 32 | 34 | const dryRun = argv.indexOf('--dry-run') > -1; |
|---|
| 33 | | -// 默认不改 manifest.json:上传用 --appid 指定目标小程序,编译产物里的 appid 不参与校验。 |
|---|
| 34 | | -// 若遇到上传失败/appid 不一致,加 --sync-manifest 先同步 manifest 再发布。 |
|---|
| 35 | | -const syncManifest = argv.indexOf('--sync-manifest') > -1; |
|---|
| 36 | | -const OWN_ARGS = ['--local', '--dry-run', '--sync-manifest']; |
|---|
| 37 | | -// 透传给 cli 的额外参数(保留 "参数 值" 的成对关系,如 --robot 2) |
|---|
| 35 | + |
|---|
| 36 | +const OWN_ARGS = ['--sync', '--no-open', '--dry-run']; |
|---|
| 37 | +// 透传给 HBuilderX cli 的额外参数(保留 "参数 值" 成对关系) |
|---|
| 38 | 38 | const passthrough = []; |
|---|
| 39 | 39 | let envNameSkipped = false; |
|---|
| 40 | 40 | argv.forEach(function(a) { |
|---|
| .. | .. |
|---|
| 47 | 47 | }); |
|---|
| 48 | 48 | |
|---|
| 49 | 49 | function fail(msg) { |
|---|
| 50 | | - console.error('[publish] ' + msg); |
|---|
| 50 | + console.error('[release] ' + msg); |
|---|
| 51 | 51 | process.exit(1); |
|---|
| 52 | 52 | } |
|---|
| 53 | 53 | |
|---|
| 54 | | -function gitShortHash() { |
|---|
| 55 | | - const res = spawnSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: ROOT, encoding: 'utf8' }); |
|---|
| 56 | | - return res.status === 0 ? String(res.stdout).trim() : ''; |
|---|
| 54 | +function readAppId(file) { |
|---|
| 55 | + try { |
|---|
| 56 | + return JSON.parse(fs.readFileSync(file, 'utf8')).appid || ''; |
|---|
| 57 | + } catch (e) { |
|---|
| 58 | + return ''; |
|---|
| 59 | + } |
|---|
| 57 | 60 | } |
|---|
| 58 | 61 | |
|---|
| 59 | | -function readManifestVersion() { |
|---|
| 60 | | - try { |
|---|
| 61 | | - const txt = fs.readFileSync(path.join(ROOT, 'manifest.json'), 'utf8'); |
|---|
| 62 | | - const m = txt.match(/"versionName"\s*:\s*"([^"]*)"/); |
|---|
| 63 | | - return m ? m[1] : '1.0.0'; |
|---|
| 64 | | - } catch (e) { |
|---|
| 65 | | - return '1.0.0'; |
|---|
| 66 | | - } |
|---|
| 62 | +/** 回写 appid(正则替换,保留原格式) */ |
|---|
| 63 | +function writeAppId(file, appid) { |
|---|
| 64 | + const prev = fs.readFileSync(file, 'utf8'); |
|---|
| 65 | + const next = prev.replace(/"appid"(\s*:\s*)"[^"]*"/, function(m, sep) { |
|---|
| 66 | + return '"appid"' + sep + '"' + appid + '"'; |
|---|
| 67 | + }); |
|---|
| 68 | + if (next !== prev) fs.writeFileSync(file, next, 'utf8'); |
|---|
| 69 | + return readAppId(file); |
|---|
| 67 | 70 | } |
|---|
| 68 | 71 | |
|---|
| 69 | 72 | function main() { |
|---|
| .. | .. |
|---|
| 76 | 79 | |
|---|
| 77 | 80 | const env = require(path.join(ENV_DIR, envName + '.js')); |
|---|
| 78 | 81 | |
|---|
| 79 | | - // 1) 校验 cli.exe |
|---|
| 80 | 82 | if (!fs.existsSync(HX_CLI)) { |
|---|
| 81 | 83 | fail('未找到 HBuilderX cli.exe:' + HX_CLI + |
|---|
| 82 | | - '\n 请确认 HBuilderX 安装路径,或设置环境变量 HBUILDERX_CLI 指向 cli.exe'); |
|---|
| 84 | + '\n 请确认安装路径,或设置环境变量 HBUILDERX_CLI 指向 cli.exe'); |
|---|
| 85 | + } |
|---|
| 86 | + if (!noOpen && !fs.existsSync(WX_CLI)) { |
|---|
| 87 | + fail('未找到微信开发者工具 cli.bat:' + WX_CLI + |
|---|
| 88 | + '\n 请设置环境变量 WX_DEVTOOLS_CLI,或加 --no-open 自行打开产物目录'); |
|---|
| 83 | 89 | } |
|---|
| 84 | 90 | |
|---|
| 85 | | - // 2) 组装 cli 参数 |
|---|
| 86 | | - const cliArgs = ['publish', '--platform', 'mp-weixin', '--project', ROOT]; |
|---|
| 91 | + console.log('[release] 环境 : ' + envName + '(' + env.ENV_NAME + ')'); |
|---|
| 92 | + console.log('[release] 小程序appid : ' + env.APPID); |
|---|
| 93 | + console.log('[release] 接口地址 : ' + env.HTTP_REQUEST_URL); |
|---|
| 94 | + console.log('[release] 产物目录 : ' + DIST_DIR); |
|---|
| 95 | + console.log('[release] manifest.json: ' + (syncManifest ? '同步为 ' + envName + ' 的 appid(会产生 git 改动)' : '保持不动(仓库维持基线,无 git 改动)')); |
|---|
| 87 | 96 | |
|---|
| 88 | | - if (!localOnly) { |
|---|
| 89 | | - const keyFile = path.join(ROOT, 'keys', 'private.' + envName + '.key'); |
|---|
| 90 | | - if (!fs.existsSync(keyFile)) { |
|---|
| 91 | | - fail('未找到上传密钥文件:' + keyFile + |
|---|
| 92 | | - '\n 获取方式:微信公众平台 → 开发管理 → 开发设置 → 小程序代码上传 → 生成上传密钥' + |
|---|
| 93 | | - '\n 若只想本地编译不上传,请加 --local'); |
|---|
| 94 | | - } |
|---|
| 95 | | - const stamp = new Date().toISOString().slice(0, 16).replace('T', ' '); |
|---|
| 96 | | - const hash = gitShortHash(); |
|---|
| 97 | | - cliArgs.push('--upload', 'true'); |
|---|
| 98 | | - cliArgs.push('--appid', env.APPID); |
|---|
| 99 | | - cliArgs.push('--privatekey', keyFile); |
|---|
| 100 | | - cliArgs.push('--version', readManifestVersion()); |
|---|
| 101 | | - cliArgs.push('--description', '[' + env.ENV_NAME + '] ' + stamp + (hash ? ' ' + hash : '')); |
|---|
| 102 | | - } |
|---|
| 103 | | - |
|---|
| 104 | | - console.log('[publish] 环境 : ' + envName + '(' + env.ENV_NAME + ')'); |
|---|
| 105 | | - console.log('[publish] 小程序appid: ' + env.APPID); |
|---|
| 106 | | - console.log('[publish] 接口地址 : ' + env.HTTP_REQUEST_URL); |
|---|
| 107 | | - console.log('[publish] 产物目录 : ' + path.join('unpackage', 'dist', 'build', 'mp-weixin')); |
|---|
| 108 | | - console.log('[publish] 模式 : ' + (localOnly ? '仅本地编译' : '编译 + 上传到微信平台')); |
|---|
| 109 | | - console.log('[publish] DCloud标识 : ' + env.UNI_APPID + '(manifest.json 里的值,仅影响非小程序端与云服务识别)'); |
|---|
| 110 | | - console.log('[publish] 命令 : "' + HX_CLI + '" ' + cliArgs.concat(passthrough).join(' ')); |
|---|
| 97 | + const cliArgs = ['publish', '--platform', 'mp-weixin', '--project', ROOT].concat(passthrough); |
|---|
| 98 | + // 路径含空格/中文,交给 cmd 解析更稳 |
|---|
| 99 | + const openCmd = '"' + WX_CLI + '" open --project "' + DIST_DIR + '"'; |
|---|
| 111 | 100 | |
|---|
| 112 | 101 | if (dryRun) { |
|---|
| 113 | | - console.log('[publish] --dry-run,未执行'); |
|---|
| 102 | + console.log('[release] 1) 编译: "' + HX_CLI + '" ' + cliArgs.join(' ')); |
|---|
| 103 | + console.log('[release] 2) 产物 appid 回写为 ' + env.APPID + ' → ' + DIST_CONFIG); |
|---|
| 104 | + if (!noOpen) console.log('[release] 3) 打开: ' + openCmd); |
|---|
| 105 | + console.log('[release] --dry-run,未执行'); |
|---|
| 114 | 106 | return; |
|---|
| 115 | 107 | } |
|---|
| 116 | 108 | |
|---|
| 117 | | - // 3) 可选:先把 appid 同步进 manifest.json(走 set-env 的同一套逻辑) |
|---|
| 109 | + // 1) 可选:同步 manifest.json(会让被跟踪文件产生改动,默认不做) |
|---|
| 118 | 110 | if (syncManifest) { |
|---|
| 119 | | - console.log('[publish] --sync-manifest:先同步 manifest.json 的 appid'); |
|---|
| 111 | + console.log('[release] 同步 manifest.json 的 appid…'); |
|---|
| 120 | 112 | const sync = spawnSync(process.execPath, [path.join(__dirname, 'set-env.js'), envName], { cwd: ROOT, stdio: 'inherit' }); |
|---|
| 121 | 113 | if (sync.status !== 0) fail('同步 manifest.json 失败'); |
|---|
| 122 | 114 | } |
|---|
| 123 | 115 | |
|---|
| 124 | | - // 4) 执行(HBuilderX 会在其控制台输出编译与上传日志) |
|---|
| 125 | | - const res = spawnSync(HX_CLI, cliArgs.concat(passthrough), { cwd: ROOT, stdio: 'inherit' }); |
|---|
| 116 | + // 2) 发行编译(产物:unpackage/dist/build/mp-weixin) |
|---|
| 117 | + console.log('[release] 开始发行编译,日志见 HBuilderX 控制台…'); |
|---|
| 118 | + const build = spawnSync(HX_CLI, cliArgs, { cwd: ROOT, stdio: 'inherit' }); |
|---|
| 119 | + if (build.error) fail('调用 cli.exe 失败:' + build.error.message); |
|---|
| 120 | + if (build.status !== 0) fail('cli.exe 退出码 ' + build.status + ',请查看 HBuilderX 控制台输出'); |
|---|
| 126 | 121 | |
|---|
| 127 | | - if (res.error) fail('调用 cli.exe 失败:' + res.error.message); |
|---|
| 128 | | - if (res.status !== 0) fail('cli.exe 退出码 ' + res.status + ',请查看 HBuilderX 控制台输出'); |
|---|
| 122 | + // 3) 校正产物 appid(--sync 时通常已一致,这里兜底保证) |
|---|
| 123 | + if (!fs.existsSync(DIST_CONFIG)) { |
|---|
| 124 | + fail('发行产物缺少 project.config.json:' + DIST_CONFIG + '\n 请确认 HBuilderX 编译是否成功'); |
|---|
| 125 | + } |
|---|
| 126 | + const before = readAppId(DIST_CONFIG); |
|---|
| 127 | + const after = writeAppId(DIST_CONFIG, env.APPID); |
|---|
| 128 | + if (!after) fail('回写产物 appid 失败:' + DIST_CONFIG); |
|---|
| 129 | + console.log('[release] 产物 appid : ' + before + (before === after ? '(无需调整)' : ' → ' + after)); |
|---|
| 129 | 130 | |
|---|
| 130 | | - console.log('[publish] 完成。若上传成功,可在微信公众平台 → 版本管理 中看到对应版本'); |
|---|
| 131 | + // 4) 用微信开发者工具打开产物(首次需在「设置-安全设置」中开启服务端口) |
|---|
| 132 | + if (!noOpen) { |
|---|
| 133 | + console.log('[release] 打开微信开发者工具…'); |
|---|
| 134 | + const open = spawnSync(openCmd, { cwd: ROOT, stdio: 'inherit', shell: true }); |
|---|
| 135 | + if (open.error || open.status !== 0) { |
|---|
| 136 | + console.log('[release] 自动打开失败(常见原因:未开启服务端口、微信开发者工具版本较旧)'); |
|---|
| 137 | + console.log('[release] 请手动打开微信开发者工具 → 导入项目 → 选择目录:' + DIST_DIR); |
|---|
| 138 | + } |
|---|
| 139 | + } |
|---|
| 140 | + |
|---|
| 141 | + console.log('[release] 完成。微信开发者工具中的项目 appid = ' + after + ',包内会自动使用 ' + env.HTTP_REQUEST_URL); |
|---|
| 131 | 142 | } |
|---|
| 132 | 143 | |
|---|
| 133 | 144 | main(); |
|---|