| .. | .. |
|---|
| 12 | 12 | * 本脚本只负责让“编译期 appid”与目标环境一致 —— 微信开发者工具打开的产物 appid, |
|---|
| 13 | 13 | * 就是 unpackage/dist/build/mp-weixin/project.config.json 里的 appid(构建时由 manifest 生成)。 |
|---|
| 14 | 14 | * |
|---|
| 15 | | - * 默认策略:不动 manifest.json(仓库保持 prod 基线,git 无噪音),只回写未跟踪的产物文件。 |
|---|
| 15 | + * 默认策略:不动 manifest.json(仓库保持基线,git 无噪音),只回写未跟踪的产物文件。 |
|---|
| 16 | + * |
|---|
| 17 | + * 外部工具(HBuilderX cli.exe、微信开发者工具 cli.bat)不写死绝对路径,按顺序自动查找: |
|---|
| 18 | + * 1) 环境变量 HBUILDERX_CLI / WX_DEVTOOLS_CLI |
|---|
| 19 | + * 2) 系统 PATH |
|---|
| 20 | + * 3) 常见安装目录(Program Files、用户目录、各磁盘常见软件目录) |
|---|
| 21 | + * 4) 磁盘根目录浅层扫描(≤2 层) |
|---|
| 22 | + * 找到后会把其所在目录加入本次进程 PATH(便于按名字直接调用),找不到则报错并给出解决办法。 |
|---|
| 16 | 23 | */ |
|---|
| 17 | 24 | const { spawnSync } = require('child_process'); |
|---|
| 18 | 25 | const fs = require('fs'); |
|---|
| .. | .. |
|---|
| 23 | 30 | const DIST_DIR = path.join(ROOT, 'unpackage', 'dist', 'build', 'mp-weixin'); |
|---|
| 24 | 31 | const DIST_CONFIG = path.join(DIST_DIR, 'project.config.json'); |
|---|
| 25 | 32 | |
|---|
| 26 | | -// 外部工具路径:可用环境变量覆盖,便于换机器 |
|---|
| 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'; |
|---|
| 33 | +/* ------------------------- 外部工具定位 ------------------------- */ |
|---|
| 34 | + |
|---|
| 35 | +// 扫描时要跳过的系统/无关目录 |
|---|
| 36 | +const SKIP_DIRS = /^(\$recycle\.bin|system volume information|windows|winsxs|node_modules|\.git|\.cache|programdata)$/i; |
|---|
| 37 | + |
|---|
| 38 | +const HX_TOOL = { |
|---|
| 39 | + envKey: 'HBUILDERX_CLI', |
|---|
| 40 | + label: 'HBuilderX 命令行 cli.exe', |
|---|
| 41 | + names: ['cli.exe'], |
|---|
| 42 | + subDirs: ['HBuilderX', 'HBuilderX\\HBuilderX'], |
|---|
| 43 | + dirPattern: /^hbuilderx/i, |
|---|
| 44 | + ok: function(file) { return /hbuilderx/i.test(file); }, |
|---|
| 45 | + hint: 'HBuilderX 安装/解压目录下的 cli.exe(如 X:\\HBuilderX\\cli.exe)', |
|---|
| 46 | + extra: '' |
|---|
| 47 | +}; |
|---|
| 48 | + |
|---|
| 49 | +const WX_TOOL = { |
|---|
| 50 | + envKey: 'WX_DEVTOOLS_CLI', |
|---|
| 51 | + label: '微信开发者工具命令行 cli.bat', |
|---|
| 52 | + names: ['cli.bat'], |
|---|
| 53 | + subDirs: ['微信web开发者工具', '微信开发者工具', 'Tencent\\微信web开发者工具', 'Tencent\\微信开发者工具'], |
|---|
| 54 | + dirPattern: /微信(web)?开发者工具/, |
|---|
| 55 | + hint: '微信开发者工具安装目录下的 cli.bat(如 X:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat)', |
|---|
| 56 | + extra: ' 3) 或加 --no-open,只编译不自动打开产物\n' |
|---|
| 57 | +}; |
|---|
| 58 | + |
|---|
| 59 | +function fail(msg) { |
|---|
| 60 | + console.error('[release] ' + msg); |
|---|
| 61 | + process.exit(1); |
|---|
| 62 | +} |
|---|
| 63 | + |
|---|
| 64 | +function uniq(list) { |
|---|
| 65 | + return list.filter(function(v, i) { return v && list.indexOf(v) === i; }); |
|---|
| 66 | +} |
|---|
| 67 | + |
|---|
| 68 | +function isFile(p) { |
|---|
| 69 | + try { return fs.statSync(p).isFile(); } catch (e) { return false; } |
|---|
| 70 | +} |
|---|
| 71 | + |
|---|
| 72 | +function isDir(p) { |
|---|
| 73 | + try { return fs.statSync(p).isDirectory(); } catch (e) { return false; } |
|---|
| 74 | +} |
|---|
| 75 | + |
|---|
| 76 | +/** 本地固定磁盘(C:、D: …);拿不到时退回环境变量里的盘符(结果缓存,只探测一次) */ |
|---|
| 77 | +let DRIVES = null; |
|---|
| 78 | +function fixedDrives() { |
|---|
| 79 | + if (DRIVES) return DRIVES; |
|---|
| 80 | + const ps = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', |
|---|
| 81 | + "[System.IO.DriveInfo]::GetDrives() | Where-Object { $_.DriveType -eq 'Fixed' } | ForEach-Object { $_.Name }" |
|---|
| 82 | + ], { encoding: 'utf8', windowsHide: true }); |
|---|
| 83 | + const fromPs = (ps.stdout || '').split(/\r?\n/) |
|---|
| 84 | + .map(function(s) { return s.trim().replace(/\\$/, ''); }) |
|---|
| 85 | + .filter(function(s) { return /^[A-Za-z]:$/.test(s); }); |
|---|
| 86 | + const fromEnv = [process.env.SystemDrive, process.env.HOMEDRIVE]; |
|---|
| 87 | + DRIVES = uniq(fromPs.concat(fromEnv).map(function(d) { return (d || '').replace(/\\$/, ''); })); |
|---|
| 88 | + return DRIVES; |
|---|
| 89 | +} |
|---|
| 90 | + |
|---|
| 91 | +/** 在系统 PATH 中按文件名查找,ok() 用于排除同名无关程序 */ |
|---|
| 92 | +function findOnPath(names, ok) { |
|---|
| 93 | + const dirs = (process.env.PATH || '').split(path.delimiter); |
|---|
| 94 | + for (let i = 0; i < dirs.length; i++) { |
|---|
| 95 | + const dir = dirs[i].replace(/^"|"$/g, ''); |
|---|
| 96 | + if (!dir) continue; |
|---|
| 97 | + for (let j = 0; j < names.length; j++) { |
|---|
| 98 | + const file = path.join(dir, names[j]); |
|---|
| 99 | + if (isFile(file) && (!ok || ok(file))) return file; |
|---|
| 100 | + } |
|---|
| 101 | + } |
|---|
| 102 | + return ''; |
|---|
| 103 | +} |
|---|
| 104 | + |
|---|
| 105 | +/** 常见安装根目录:Program Files、用户目录、各磁盘根与常见软件目录 */ |
|---|
| 106 | +function baseDirs() { |
|---|
| 107 | + const bases = [ |
|---|
| 108 | + process.env.ProgramFiles, |
|---|
| 109 | + process.env['ProgramFiles(x86)'], |
|---|
| 110 | + process.env.LOCALAPPDATA, |
|---|
| 111 | + process.env.APPDATA, |
|---|
| 112 | + process.env.USERPROFILE, |
|---|
| 113 | + process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Programs'), |
|---|
| 114 | + process.env.USERPROFILE && path.join(process.env.USERPROFILE, 'Desktop'), |
|---|
| 115 | + process.env.USERPROFILE && path.join(process.env.USERPROFILE, 'Downloads') |
|---|
| 116 | + ]; |
|---|
| 117 | + fixedDrives().forEach(function(d) { |
|---|
| 118 | + bases.push(d + '\\', d + '\\Program Files', d + '\\Program Files (x86)', |
|---|
| 119 | + d + '\\soft', d + '\\software', d + '\\Programs', d + '\\develop'); |
|---|
| 120 | + }); |
|---|
| 121 | + return uniq(bases).filter(isDir); |
|---|
| 122 | +} |
|---|
| 123 | + |
|---|
| 124 | +/** 磁盘浅层扫描:找名字符合特征的目录(兜底手段) */ |
|---|
| 125 | +function scanPlausibleDirs(pattern) { |
|---|
| 126 | + const found = []; |
|---|
| 127 | + fixedDrives().forEach(function(root) { |
|---|
| 128 | + let level1 = []; |
|---|
| 129 | + try { level1 = fs.readdirSync(root + '\\', { withFileTypes: true }); } catch (e) { return; } |
|---|
| 130 | + level1.forEach(function(e1) { |
|---|
| 131 | + if (!e1.isDirectory() || SKIP_DIRS.test(e1.name)) return; |
|---|
| 132 | + const p1 = path.join(root + '\\', e1.name); |
|---|
| 133 | + if (pattern.test(e1.name)) { found.push(p1); return; } |
|---|
| 134 | + let level2 = []; |
|---|
| 135 | + try { level2 = fs.readdirSync(p1, { withFileTypes: true }); } catch (e) { return; } |
|---|
| 136 | + level2.forEach(function(e2) { |
|---|
| 137 | + if (e2.isDirectory() && pattern.test(e2.name)) found.push(path.join(p1, e2.name)); |
|---|
| 138 | + }); |
|---|
| 139 | + }); |
|---|
| 140 | + }); |
|---|
| 141 | + return found; |
|---|
| 142 | +} |
|---|
| 143 | + |
|---|
| 144 | +/** 定位工具:返回 { file, source },找不到返回 null */ |
|---|
| 145 | +function resolveTool(cfg) { |
|---|
| 146 | + // 1) 环境变量显式指定 |
|---|
| 147 | + const fromEnv = (process.env[cfg.envKey] || '').trim(); |
|---|
| 148 | + if (fromEnv) { |
|---|
| 149 | + if (isFile(fromEnv)) return { file: fromEnv, source: '环境变量 ' + cfg.envKey }; |
|---|
| 150 | + fail('环境变量 ' + cfg.envKey + ' 指向的文件不存在:' + fromEnv); |
|---|
| 151 | + } |
|---|
| 152 | + |
|---|
| 153 | + // 2) 系统 PATH |
|---|
| 154 | + const onPath = findOnPath(cfg.names, cfg.ok); |
|---|
| 155 | + if (onPath) return { file: onPath, source: '系统 PATH' }; |
|---|
| 156 | + |
|---|
| 157 | + // 3) 常见安装目录 + 磁盘浅层扫描 |
|---|
| 158 | + const dirs = baseDirs().concat(scanPlausibleDirs(cfg.dirPattern)); |
|---|
| 159 | + for (let i = 0; i < dirs.length; i++) { |
|---|
| 160 | + for (let j = 0; j < cfg.subDirs.length; j++) { |
|---|
| 161 | + for (let k = 0; k < cfg.names.length; k++) { |
|---|
| 162 | + const file = path.join(dirs[i], cfg.subDirs[j], cfg.names[k]); |
|---|
| 163 | + if (isFile(file)) return { file: file, source: '常见安装目录' }; |
|---|
| 164 | + } |
|---|
| 165 | + } |
|---|
| 166 | + } |
|---|
| 167 | + return null; |
|---|
| 168 | +} |
|---|
| 169 | + |
|---|
| 170 | +/** 找不到工具时的报错提示 */ |
|---|
| 171 | +function toolFail(cfg) { |
|---|
| 172 | + fail('未找到 ' + cfg.label + '\n' + |
|---|
| 173 | + ' 已查找:环境变量 ' + cfg.envKey + ' → 系统 PATH → 常见安装目录 → 磁盘浅层扫描\n' + |
|---|
| 174 | + ' 典型位置:' + cfg.hint + '\n' + |
|---|
| 175 | + ' 解决办法(任选其一):\n' + |
|---|
| 176 | + ' 1) 把安装目录加入 PATH:set PATH=%PATH%;<安装目录>\n' + |
|---|
| 177 | + ' 2) 用环境变量指定:set ' + cfg.envKey + '=<cli 完整路径>\n' + |
|---|
| 178 | + cfg.extra); |
|---|
| 179 | +} |
|---|
| 180 | + |
|---|
| 181 | +/** 把工具所在目录加入本次进程 PATH,便于按名字直接调用 */ |
|---|
| 182 | +function putOnPath(file) { |
|---|
| 183 | + const dir = path.dirname(file); |
|---|
| 184 | + const cur = process.env.PATH || ''; |
|---|
| 185 | + if (cur.split(path.delimiter).indexOf(dir) < 0) process.env.PATH = dir + path.delimiter + cur; |
|---|
| 186 | +} |
|---|
| 187 | + |
|---|
| 188 | +/* ------------------------- 参数解析 ------------------------- */ |
|---|
| 29 | 189 | |
|---|
| 30 | 190 | const argv = process.argv.slice(2); |
|---|
| 31 | 191 | const envName = (argv.find(function(a) { return a.charAt(0) !== '-'; }) || '').trim(); |
|---|
| .. | .. |
|---|
| 45 | 205 | } |
|---|
| 46 | 206 | passthrough.push(a); |
|---|
| 47 | 207 | }); |
|---|
| 48 | | - |
|---|
| 49 | | -function fail(msg) { |
|---|
| 50 | | - console.error('[release] ' + msg); |
|---|
| 51 | | - process.exit(1); |
|---|
| 52 | | -} |
|---|
| 53 | 208 | |
|---|
| 54 | 209 | function readAppId(file) { |
|---|
| 55 | 210 | try { |
|---|
| .. | .. |
|---|
| 79 | 234 | |
|---|
| 80 | 235 | const env = require(path.join(ENV_DIR, envName + '.js')); |
|---|
| 81 | 236 | |
|---|
| 82 | | - if (!fs.existsSync(HX_CLI)) { |
|---|
| 83 | | - fail('未找到 HBuilderX cli.exe:' + HX_CLI + |
|---|
| 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 自行打开产物目录'); |
|---|
| 89 | | - } |
|---|
| 237 | + // 定位外部工具(不写死路径) |
|---|
| 238 | + console.log('[release] 查找 HBuilderX / 微信开发者工具…'); |
|---|
| 239 | + const hx = resolveTool(HX_TOOL); |
|---|
| 240 | + if (!hx) toolFail(HX_TOOL); |
|---|
| 241 | + const wx = noOpen ? null : resolveTool(WX_TOOL); |
|---|
| 242 | + if (!noOpen && !wx) toolFail(WX_TOOL); |
|---|
| 243 | + putOnPath(hx.file); |
|---|
| 244 | + if (wx) putOnPath(wx.file); |
|---|
| 90 | 245 | |
|---|
| 91 | 246 | console.log('[release] 环境 : ' + envName + '(' + env.ENV_NAME + ')'); |
|---|
| 92 | 247 | console.log('[release] 小程序appid : ' + env.APPID); |
|---|
| 93 | 248 | console.log('[release] 接口地址 : ' + env.HTTP_REQUEST_URL); |
|---|
| 94 | 249 | console.log('[release] 产物目录 : ' + DIST_DIR); |
|---|
| 250 | + console.log('[release] HBuilderX : ' + hx.file + '(' + hx.source + ')'); |
|---|
| 251 | + if (wx) console.log('[release] 开发者工具 : ' + wx.file + '(' + wx.source + ')'); |
|---|
| 95 | 252 | console.log('[release] manifest.json: ' + (syncManifest ? '同步为 ' + envName + ' 的 appid(会产生 git 改动)' : '保持不动(仓库维持基线,无 git 改动)')); |
|---|
| 96 | 253 | |
|---|
| 97 | 254 | const cliArgs = ['publish', '--platform', 'mp-weixin', '--project', ROOT].concat(passthrough); |
|---|
| 98 | 255 | // 路径含空格/中文,交给 cmd 解析更稳 |
|---|
| 99 | | - const openCmd = '"' + WX_CLI + '" open --project "' + DIST_DIR + '"'; |
|---|
| 256 | + const openCmd = '"' + wx.file + '" open --project "' + DIST_DIR + '"'; |
|---|
| 100 | 257 | |
|---|
| 101 | 258 | if (dryRun) { |
|---|
| 102 | | - console.log('[release] 1) 编译: "' + HX_CLI + '" ' + cliArgs.join(' ')); |
|---|
| 259 | + console.log('[release] 1) 编译: "' + hx.file + '" ' + cliArgs.join(' ')); |
|---|
| 103 | 260 | console.log('[release] 2) 产物 appid 回写为 ' + env.APPID + ' → ' + DIST_CONFIG); |
|---|
| 104 | 261 | if (!noOpen) console.log('[release] 3) 打开: ' + openCmd); |
|---|
| 105 | 262 | console.log('[release] --dry-run,未执行'); |
|---|
| .. | .. |
|---|
| 115 | 272 | |
|---|
| 116 | 273 | // 2) 发行编译(产物:unpackage/dist/build/mp-weixin) |
|---|
| 117 | 274 | console.log('[release] 开始发行编译,日志见 HBuilderX 控制台…'); |
|---|
| 118 | | - const build = spawnSync(HX_CLI, cliArgs, { cwd: ROOT, stdio: 'inherit' }); |
|---|
| 275 | + const build = spawnSync(hx.file, cliArgs, { cwd: ROOT, stdio: 'inherit' }); |
|---|
| 119 | 276 | if (build.error) fail('调用 cli.exe 失败:' + build.error.message); |
|---|
| 120 | 277 | if (build.status !== 0) fail('cli.exe 退出码 ' + build.status + ',请查看 HBuilderX 控制台输出'); |
|---|
| 121 | 278 | |
|---|