| .. | .. |
|---|
| 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 --no-gen # 不改 manifest.json(要求它已存在) |
|---|
| 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 | + * 流程:从 manifest.json.demo 生成 manifest.json(按目标环境注入 appid)→ 发行编译 → |
|---|
| 12 | + * 校正产物 appid → 用微信开发者工具打开产物 |
|---|
| 14 | 13 | * |
|---|
| 15 | | - * 密钥文件:keys/private.<env>.key |
|---|
| 16 | | - * 获取方式:微信公众平台 → 开发管理 → 开发设置 → 小程序代码上传 → 生成上传密钥 |
|---|
| 17 | | - * 注意:密钥属于机密,已通过 .gitignore 排除,切勿提交 |
|---|
| 14 | + * 环境值统一取自 config/env/<env>.js(唯一数据源),接口地址等运行时参数由包内按 appid 自动识别; |
|---|
| 15 | + * manifest.json 是生成物(已 .gitignore),因此 git 永远没有环境噪音。 |
|---|
| 16 | + * 微信开发者工具打开的产物 appid = unpackage/dist/build/mp-weixin/project.config.json 的 appid |
|---|
| 17 | + * (构建时由 manifest 生成,这里再兜底校正一次)。 |
|---|
| 18 | + * |
|---|
| 19 | + * 外部工具(HBuilderX cli.exe、微信开发者工具 cli.bat)不写死绝对路径,按顺序自动查找: |
|---|
| 20 | + * 1) 环境变量 HBUILDERX_CLI / WX_DEVTOOLS_CLI |
|---|
| 21 | + * 2) 系统 PATH |
|---|
| 22 | + * 3) 常见安装目录(Program Files、用户目录、各磁盘常见软件目录) |
|---|
| 23 | + * 4) 磁盘根目录浅层扫描(≤2 层) |
|---|
| 24 | + * 找到后会把其所在目录加入本次进程 PATH(便于按名字直接调用),找不到则报错并给出解决办法。 |
|---|
| 18 | 25 | */ |
|---|
| 19 | 26 | const { spawnSync } = require('child_process'); |
|---|
| 20 | 27 | const fs = require('fs'); |
|---|
| 21 | 28 | const path = require('path'); |
|---|
| 29 | +const manifest = require('./manifest'); |
|---|
| 22 | 30 | |
|---|
| 23 | 31 | const ROOT = path.resolve(__dirname, '..'); |
|---|
| 24 | 32 | const ENV_DIR = path.join(ROOT, 'config', 'env'); |
|---|
| 33 | +const DIST_DIR = path.join(ROOT, 'unpackage', 'dist', 'build', 'mp-weixin'); |
|---|
| 34 | +const DIST_CONFIG = path.join(DIST_DIR, 'project.config.json'); |
|---|
| 25 | 35 | |
|---|
| 26 | | -// HBuilderX cli.exe 路径:优先读环境变量,便于换机器 / 换安装位置 |
|---|
| 27 | | -const HX_CLI = process.env.HBUILDERX_CLI || 'C:\\Users\\Administrator\\Desktop\\HBuilderX\\cli.exe'; |
|---|
| 36 | +/* ------------------------- 外部工具定位 ------------------------- */ |
|---|
| 37 | + |
|---|
| 38 | +// 扫描时要跳过的系统/无关目录 |
|---|
| 39 | +const SKIP_DIRS = /^(\$recycle\.bin|system volume information|windows|winsxs|node_modules|\.git|\.cache|programdata)$/i; |
|---|
| 40 | + |
|---|
| 41 | +const HX_TOOL = { |
|---|
| 42 | + envKey: 'HBUILDERX_CLI', |
|---|
| 43 | + label: 'HBuilderX 命令行 cli.exe', |
|---|
| 44 | + names: ['cli.exe'], |
|---|
| 45 | + subDirs: ['HBuilderX', 'HBuilderX\\HBuilderX'], |
|---|
| 46 | + dirPattern: /^hbuilderx/i, |
|---|
| 47 | + ok: function(file) { return /hbuilderx/i.test(file); }, |
|---|
| 48 | + hint: 'HBuilderX 安装/解压目录下的 cli.exe(如 X:\\HBuilderX\\cli.exe)', |
|---|
| 49 | + extra: '' |
|---|
| 50 | +}; |
|---|
| 51 | + |
|---|
| 52 | +const WX_TOOL = { |
|---|
| 53 | + envKey: 'WX_DEVTOOLS_CLI', |
|---|
| 54 | + label: '微信开发者工具命令行 cli.bat', |
|---|
| 55 | + names: ['cli.bat'], |
|---|
| 56 | + subDirs: ['微信web开发者工具', '微信开发者工具', 'Tencent\\微信web开发者工具', 'Tencent\\微信开发者工具'], |
|---|
| 57 | + dirPattern: /微信(web)?开发者工具/, |
|---|
| 58 | + hint: '微信开发者工具安装目录下的 cli.bat(如 X:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat)', |
|---|
| 59 | + extra: ' 3) 或加 --no-open,只编译不自动打开产物\n' |
|---|
| 60 | +}; |
|---|
| 61 | + |
|---|
| 62 | +function fail(msg) { |
|---|
| 63 | + console.error('[release] ' + msg); |
|---|
| 64 | + process.exit(1); |
|---|
| 65 | +} |
|---|
| 66 | + |
|---|
| 67 | +function uniq(list) { |
|---|
| 68 | + return list.filter(function(v, i) { return v && list.indexOf(v) === i; }); |
|---|
| 69 | +} |
|---|
| 70 | + |
|---|
| 71 | +function isFile(p) { |
|---|
| 72 | + try { return fs.statSync(p).isFile(); } catch (e) { return false; } |
|---|
| 73 | +} |
|---|
| 74 | + |
|---|
| 75 | +function isDir(p) { |
|---|
| 76 | + try { return fs.statSync(p).isDirectory(); } catch (e) { return false; } |
|---|
| 77 | +} |
|---|
| 78 | + |
|---|
| 79 | +/** 本地固定磁盘(C:、D: …);拿不到时退回环境变量里的盘符(结果缓存,只探测一次) */ |
|---|
| 80 | +let DRIVES = null; |
|---|
| 81 | +function fixedDrives() { |
|---|
| 82 | + if (DRIVES) return DRIVES; |
|---|
| 83 | + const ps = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', |
|---|
| 84 | + "[System.IO.DriveInfo]::GetDrives() | Where-Object { $_.DriveType -eq 'Fixed' } | ForEach-Object { $_.Name }" |
|---|
| 85 | + ], { encoding: 'utf8', windowsHide: true }); |
|---|
| 86 | + const fromPs = (ps.stdout || '').split(/\r?\n/) |
|---|
| 87 | + .map(function(s) { return s.trim().replace(/\\$/, ''); }) |
|---|
| 88 | + .filter(function(s) { return /^[A-Za-z]:$/.test(s); }); |
|---|
| 89 | + const fromEnv = [process.env.SystemDrive, process.env.HOMEDRIVE]; |
|---|
| 90 | + DRIVES = uniq(fromPs.concat(fromEnv).map(function(d) { return (d || '').replace(/\\$/, ''); })); |
|---|
| 91 | + return DRIVES; |
|---|
| 92 | +} |
|---|
| 93 | + |
|---|
| 94 | +/** 在系统 PATH 中按文件名查找,ok() 用于排除同名无关程序 */ |
|---|
| 95 | +function findOnPath(names, ok) { |
|---|
| 96 | + const dirs = (process.env.PATH || '').split(path.delimiter); |
|---|
| 97 | + for (let i = 0; i < dirs.length; i++) { |
|---|
| 98 | + const dir = dirs[i].replace(/^"|"$/g, ''); |
|---|
| 99 | + if (!dir) continue; |
|---|
| 100 | + for (let j = 0; j < names.length; j++) { |
|---|
| 101 | + const file = path.join(dir, names[j]); |
|---|
| 102 | + if (isFile(file) && (!ok || ok(file))) return file; |
|---|
| 103 | + } |
|---|
| 104 | + } |
|---|
| 105 | + return ''; |
|---|
| 106 | +} |
|---|
| 107 | + |
|---|
| 108 | +/** 常见安装根目录:Program Files、用户目录、各磁盘根与常见软件目录 */ |
|---|
| 109 | +function baseDirs() { |
|---|
| 110 | + const bases = [ |
|---|
| 111 | + process.env.ProgramFiles, |
|---|
| 112 | + process.env['ProgramFiles(x86)'], |
|---|
| 113 | + process.env.LOCALAPPDATA, |
|---|
| 114 | + process.env.APPDATA, |
|---|
| 115 | + process.env.USERPROFILE, |
|---|
| 116 | + process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Programs'), |
|---|
| 117 | + process.env.USERPROFILE && path.join(process.env.USERPROFILE, 'Desktop'), |
|---|
| 118 | + process.env.USERPROFILE && path.join(process.env.USERPROFILE, 'Downloads') |
|---|
| 119 | + ]; |
|---|
| 120 | + fixedDrives().forEach(function(d) { |
|---|
| 121 | + bases.push(d + '\\', d + '\\Program Files', d + '\\Program Files (x86)', |
|---|
| 122 | + d + '\\soft', d + '\\software', d + '\\Programs', d + '\\develop'); |
|---|
| 123 | + }); |
|---|
| 124 | + return uniq(bases).filter(isDir); |
|---|
| 125 | +} |
|---|
| 126 | + |
|---|
| 127 | +/** 磁盘浅层扫描:找名字符合特征的目录(兜底手段) */ |
|---|
| 128 | +function scanPlausibleDirs(pattern) { |
|---|
| 129 | + const found = []; |
|---|
| 130 | + fixedDrives().forEach(function(root) { |
|---|
| 131 | + let level1 = []; |
|---|
| 132 | + try { level1 = fs.readdirSync(root + '\\', { withFileTypes: true }); } catch (e) { return; } |
|---|
| 133 | + level1.forEach(function(e1) { |
|---|
| 134 | + if (!e1.isDirectory() || SKIP_DIRS.test(e1.name)) return; |
|---|
| 135 | + const p1 = path.join(root + '\\', e1.name); |
|---|
| 136 | + if (pattern.test(e1.name)) { found.push(p1); return; } |
|---|
| 137 | + let level2 = []; |
|---|
| 138 | + try { level2 = fs.readdirSync(p1, { withFileTypes: true }); } catch (e) { return; } |
|---|
| 139 | + level2.forEach(function(e2) { |
|---|
| 140 | + if (e2.isDirectory() && pattern.test(e2.name)) found.push(path.join(p1, e2.name)); |
|---|
| 141 | + }); |
|---|
| 142 | + }); |
|---|
| 143 | + }); |
|---|
| 144 | + return found; |
|---|
| 145 | +} |
|---|
| 146 | + |
|---|
| 147 | +/** 定位工具:返回 { file, source },找不到返回 null */ |
|---|
| 148 | +function resolveTool(cfg) { |
|---|
| 149 | + // 1) 环境变量显式指定 |
|---|
| 150 | + const fromEnv = (process.env[cfg.envKey] || '').trim(); |
|---|
| 151 | + if (fromEnv) { |
|---|
| 152 | + if (isFile(fromEnv)) return { file: fromEnv, source: '环境变量 ' + cfg.envKey }; |
|---|
| 153 | + fail('环境变量 ' + cfg.envKey + ' 指向的文件不存在:' + fromEnv); |
|---|
| 154 | + } |
|---|
| 155 | + |
|---|
| 156 | + // 2) 系统 PATH |
|---|
| 157 | + const onPath = findOnPath(cfg.names, cfg.ok); |
|---|
| 158 | + if (onPath) return { file: onPath, source: '系统 PATH' }; |
|---|
| 159 | + |
|---|
| 160 | + // 3) 常见安装目录 + 磁盘浅层扫描 |
|---|
| 161 | + const dirs = baseDirs().concat(scanPlausibleDirs(cfg.dirPattern)); |
|---|
| 162 | + for (let i = 0; i < dirs.length; i++) { |
|---|
| 163 | + for (let j = 0; j < cfg.subDirs.length; j++) { |
|---|
| 164 | + for (let k = 0; k < cfg.names.length; k++) { |
|---|
| 165 | + const file = path.join(dirs[i], cfg.subDirs[j], cfg.names[k]); |
|---|
| 166 | + if (isFile(file)) return { file: file, source: '常见安装目录' }; |
|---|
| 167 | + } |
|---|
| 168 | + } |
|---|
| 169 | + } |
|---|
| 170 | + return null; |
|---|
| 171 | +} |
|---|
| 172 | + |
|---|
| 173 | +/** 找不到工具时的报错提示 */ |
|---|
| 174 | +function toolFail(cfg) { |
|---|
| 175 | + fail('未找到 ' + cfg.label + '\n' + |
|---|
| 176 | + ' 已查找:环境变量 ' + cfg.envKey + ' → 系统 PATH → 常见安装目录 → 磁盘浅层扫描\n' + |
|---|
| 177 | + ' 典型位置:' + cfg.hint + '\n' + |
|---|
| 178 | + ' 解决办法(任选其一):\n' + |
|---|
| 179 | + ' 1) 把安装目录加入 PATH:set PATH=%PATH%;<安装目录>\n' + |
|---|
| 180 | + ' 2) 用环境变量指定:set ' + cfg.envKey + '=<cli 完整路径>\n' + |
|---|
| 181 | + cfg.extra); |
|---|
| 182 | +} |
|---|
| 183 | + |
|---|
| 184 | +/** 把工具所在目录加入本次进程 PATH,便于按名字直接调用 */ |
|---|
| 185 | +function putOnPath(file) { |
|---|
| 186 | + const dir = path.dirname(file); |
|---|
| 187 | + const cur = process.env.PATH || ''; |
|---|
| 188 | + if (cur.split(path.delimiter).indexOf(dir) < 0) process.env.PATH = dir + path.delimiter + cur; |
|---|
| 189 | +} |
|---|
| 190 | + |
|---|
| 191 | +/* ------------------------- 参数解析 ------------------------- */ |
|---|
| 28 | 192 | |
|---|
| 29 | 193 | const argv = process.argv.slice(2); |
|---|
| 30 | 194 | const envName = (argv.find(function(a) { return a.charAt(0) !== '-'; }) || '').trim(); |
|---|
| 31 | | -const localOnly = argv.indexOf('--local') > -1; |
|---|
| 195 | +const noGen = argv.indexOf('--no-gen') > -1; |
|---|
| 196 | +const noOpen = argv.indexOf('--no-open') > -1; |
|---|
| 32 | 197 | 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) |
|---|
| 198 | + |
|---|
| 199 | +const OWN_ARGS = ['--no-gen', '--no-open', '--dry-run']; |
|---|
| 200 | +// 透传给 HBuilderX cli 的额外参数(保留 "参数 值" 成对关系) |
|---|
| 38 | 201 | const passthrough = []; |
|---|
| 39 | 202 | let envNameSkipped = false; |
|---|
| 40 | 203 | argv.forEach(function(a) { |
|---|
| .. | .. |
|---|
| 46 | 209 | passthrough.push(a); |
|---|
| 47 | 210 | }); |
|---|
| 48 | 211 | |
|---|
| 49 | | -function fail(msg) { |
|---|
| 50 | | - console.error('[publish] ' + msg); |
|---|
| 51 | | - process.exit(1); |
|---|
| 52 | | -} |
|---|
| 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() : ''; |
|---|
| 57 | | -} |
|---|
| 58 | | - |
|---|
| 59 | | -function readManifestVersion() { |
|---|
| 212 | +function readAppId(file) { |
|---|
| 60 | 213 | 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'; |
|---|
| 214 | + return JSON.parse(fs.readFileSync(file, 'utf8')).appid || ''; |
|---|
| 64 | 215 | } catch (e) { |
|---|
| 65 | | - return '1.0.0'; |
|---|
| 216 | + return ''; |
|---|
| 66 | 217 | } |
|---|
| 218 | +} |
|---|
| 219 | + |
|---|
| 220 | +/** 回写 appid(正则替换,保留原格式) */ |
|---|
| 221 | +function writeAppId(file, appid) { |
|---|
| 222 | + const prev = fs.readFileSync(file, 'utf8'); |
|---|
| 223 | + const next = prev.replace(/"appid"(\s*:\s*)"[^"]*"/, function(m, sep) { |
|---|
| 224 | + return '"appid"' + sep + '"' + appid + '"'; |
|---|
| 225 | + }); |
|---|
| 226 | + if (next !== prev) fs.writeFileSync(file, next, 'utf8'); |
|---|
| 227 | + return readAppId(file); |
|---|
| 67 | 228 | } |
|---|
| 68 | 229 | |
|---|
| 69 | 230 | function main() { |
|---|
| .. | .. |
|---|
| 76 | 237 | |
|---|
| 77 | 238 | const env = require(path.join(ENV_DIR, envName + '.js')); |
|---|
| 78 | 239 | |
|---|
| 79 | | - // 1) 校验 cli.exe |
|---|
| 80 | | - if (!fs.existsSync(HX_CLI)) { |
|---|
| 81 | | - fail('未找到 HBuilderX cli.exe:' + HX_CLI + |
|---|
| 82 | | - '\n 请确认 HBuilderX 安装路径,或设置环境变量 HBUILDERX_CLI 指向 cli.exe'); |
|---|
| 240 | + // 定位外部工具(不写死路径) |
|---|
| 241 | + console.log('[release] 查找 HBuilderX / 微信开发者工具…'); |
|---|
| 242 | + const hx = resolveTool(HX_TOOL); |
|---|
| 243 | + if (!hx) toolFail(HX_TOOL); |
|---|
| 244 | + const wx = noOpen ? null : resolveTool(WX_TOOL); |
|---|
| 245 | + if (!noOpen && !wx) toolFail(WX_TOOL); |
|---|
| 246 | + putOnPath(hx.file); |
|---|
| 247 | + if (wx) putOnPath(wx.file); |
|---|
| 248 | + |
|---|
| 249 | + // 目标编译产物由 manifest.json 决定,而 manifest.json 是生成物 |
|---|
| 250 | + if (noGen && !fs.existsSync(manifest.TARGET)) { |
|---|
| 251 | + fail('--no-gen 需要 manifest.json 已存在,请先运行:node scripts/set-env.js ' + envName); |
|---|
| 83 | 252 | } |
|---|
| 84 | 253 | |
|---|
| 85 | | - // 2) 组装 cli 参数 |
|---|
| 86 | | - const cliArgs = ['publish', '--platform', 'mp-weixin', '--project', ROOT]; |
|---|
| 254 | + console.log('[release] 环境 : ' + envName + '(' + env.ENV_NAME + ')'); |
|---|
| 255 | + console.log('[release] 小程序appid : ' + env.APPID); |
|---|
| 256 | + console.log('[release] 接口地址 : ' + env.HTTP_REQUEST_URL); |
|---|
| 257 | + console.log('[release] 产物目录 : ' + DIST_DIR); |
|---|
| 258 | + console.log('[release] HBuilderX : ' + hx.file + '(' + hx.source + ')'); |
|---|
| 259 | + if (wx) console.log('[release] 开发者工具 : ' + wx.file + '(' + wx.source + ')'); |
|---|
| 260 | + console.log('[release] manifest.json: ' + (noGen |
|---|
| 261 | + ? '不改动(--no-gen)' |
|---|
| 262 | + : '由 manifest.json.demo 生成(appid ' + env.APPID + ',生成物不入库)')); |
|---|
| 87 | 263 | |
|---|
| 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(' ')); |
|---|
| 264 | + const cliArgs = ['publish', '--platform', 'mp-weixin', '--project', ROOT].concat(passthrough); |
|---|
| 265 | + // 路径含空格/中文,交给 cmd 解析更稳 |
|---|
| 266 | + const openCmd = '"' + wx.file + '" open --project "' + DIST_DIR + '"'; |
|---|
| 111 | 267 | |
|---|
| 112 | 268 | if (dryRun) { |
|---|
| 113 | | - console.log('[publish] --dry-run,未执行'); |
|---|
| 269 | + console.log('[release] 0) 生成: manifest.json ← manifest.json.demo' + (noGen ? '(已指定 --no-gen,跳过)' : '')); |
|---|
| 270 | + console.log('[release] 1) 编译: "' + hx.file + '" ' + cliArgs.join(' ')); |
|---|
| 271 | + console.log('[release] 2) 产物 appid 回写为 ' + env.APPID + ' → ' + DIST_CONFIG); |
|---|
| 272 | + if (!noOpen) console.log('[release] 3) 打开: ' + openCmd); |
|---|
| 273 | + console.log('[release] --dry-run,未执行'); |
|---|
| 114 | 274 | return; |
|---|
| 115 | 275 | } |
|---|
| 116 | 276 | |
|---|
| 117 | | - // 3) 可选:先把 appid 同步进 manifest.json(走 set-env 的同一套逻辑) |
|---|
| 118 | | - if (syncManifest) { |
|---|
| 119 | | - console.log('[publish] --sync-manifest:先同步 manifest.json 的 appid'); |
|---|
| 120 | | - const sync = spawnSync(process.execPath, [path.join(__dirname, 'set-env.js'), envName], { cwd: ROOT, stdio: 'inherit' }); |
|---|
| 121 | | - if (sync.status !== 0) fail('同步 manifest.json 失败'); |
|---|
| 277 | + // 1) 生成 manifest.json(HBuilderX 的编译输入,模板:manifest.json.demo) |
|---|
| 278 | + if (noGen) { |
|---|
| 279 | + console.log('[release] 跳过 manifest.json 生成(--no-gen)'); |
|---|
| 280 | + } else { |
|---|
| 281 | + manifest.generate(env); |
|---|
| 122 | 282 | } |
|---|
| 123 | 283 | |
|---|
| 124 | | - // 4) 执行(HBuilderX 会在其控制台输出编译与上传日志) |
|---|
| 125 | | - const res = spawnSync(HX_CLI, cliArgs.concat(passthrough), { cwd: ROOT, stdio: 'inherit' }); |
|---|
| 284 | + // 2) 发行编译(产物:unpackage/dist/build/mp-weixin) |
|---|
| 285 | + console.log('[release] 开始发行编译,日志见 HBuilderX 控制台…'); |
|---|
| 286 | + const build = spawnSync(hx.file, cliArgs, { cwd: ROOT, stdio: 'inherit' }); |
|---|
| 287 | + if (build.error) fail('调用 cli.exe 失败:' + build.error.message); |
|---|
| 288 | + if (build.status !== 0) fail('cli.exe 退出码 ' + build.status + ',请查看 HBuilderX 控制台输出'); |
|---|
| 126 | 289 | |
|---|
| 127 | | - if (res.error) fail('调用 cli.exe 失败:' + res.error.message); |
|---|
| 128 | | - if (res.status !== 0) fail('cli.exe 退出码 ' + res.status + ',请查看 HBuilderX 控制台输出'); |
|---|
| 290 | + // 3) 校正产物 appid(正常应与 manifest 一致,这里兜底保证) |
|---|
| 291 | + if (!fs.existsSync(DIST_CONFIG)) { |
|---|
| 292 | + fail('发行产物缺少 project.config.json:' + DIST_CONFIG + '\n 请确认 HBuilderX 编译是否成功'); |
|---|
| 293 | + } |
|---|
| 294 | + const before = readAppId(DIST_CONFIG); |
|---|
| 295 | + const after = writeAppId(DIST_CONFIG, env.APPID); |
|---|
| 296 | + if (!after) fail('回写产物 appid 失败:' + DIST_CONFIG); |
|---|
| 297 | + console.log('[release] 产物 appid : ' + before + (before === after ? '(无需调整)' : ' → ' + after)); |
|---|
| 129 | 298 | |
|---|
| 130 | | - console.log('[publish] 完成。若上传成功,可在微信公众平台 → 版本管理 中看到对应版本'); |
|---|
| 299 | + // 4) 用微信开发者工具打开产物(首次需在「设置-安全设置」中开启服务端口) |
|---|
| 300 | + if (!noOpen) { |
|---|
| 301 | + console.log('[release] 打开微信开发者工具…'); |
|---|
| 302 | + const open = spawnSync(openCmd, { cwd: ROOT, stdio: 'inherit', shell: true }); |
|---|
| 303 | + if (open.error || open.status !== 0) { |
|---|
| 304 | + console.log('[release] 自动打开失败(常见原因:未开启服务端口、微信开发者工具版本较旧)'); |
|---|
| 305 | + console.log('[release] 请手动打开微信开发者工具 → 导入项目 → 选择目录:' + DIST_DIR); |
|---|
| 306 | + } |
|---|
| 307 | + } |
|---|
| 308 | + |
|---|
| 309 | + console.log('[release] 完成。微信开发者工具中的项目 appid = ' + after + ',包内会自动使用 ' + env.HTTP_REQUEST_URL); |
|---|
| 131 | 310 | } |
|---|
| 132 | 311 | |
|---|
| 133 | 312 | main(); |
|---|