/** * 命令行发行(不上传)+ 自动用微信开发者工具打开产物 * * 用法: * node scripts/publish.js sit # 发行 SIT 版本并打开微信开发者工具 * node scripts/publish.js prod # 发行 PROD 版本并打开微信开发者工具 * node scripts/publish.js sit --no-gen # 不改 manifest.json(要求它已存在) * node scripts/publish.js sit --no-open # 只编译,不打开微信开发者工具 * node scripts/publish.js sit --dry-run # 只打印将要执行的命令 * * 流程:从 manifest.json.demo 生成 manifest.json(按目标环境注入 appid)→ 发行编译 → * 校正产物 appid → 用微信开发者工具打开产物 * * 环境值统一取自 config/env/.js(唯一数据源),接口地址等运行时参数由包内按 appid 自动识别; * manifest.json 是生成物(已 .gitignore),因此 git 永远没有环境噪音。 * 微信开发者工具打开的产物 appid = unpackage/dist/build/mp-weixin/project.config.json 的 appid * (构建时由 manifest 生成,这里再兜底校正一次)。 * * 外部工具(HBuilderX cli.exe、微信开发者工具 cli.bat)不写死绝对路径,按顺序自动查找: * 1) 环境变量 HBUILDERX_CLI / WX_DEVTOOLS_CLI * 2) 系统 PATH * 3) 常见安装目录(Program Files、用户目录、各磁盘常见软件目录) * 4) 磁盘根目录浅层扫描(≤2 层) * 找到后会把其所在目录加入本次进程 PATH(便于按名字直接调用),找不到则报错并给出解决办法。 */ const { spawnSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const manifest = require('./manifest'); const ROOT = path.resolve(__dirname, '..'); const ENV_DIR = path.join(ROOT, 'config', 'env'); const DIST_DIR = path.join(ROOT, 'unpackage', 'dist', 'build', 'mp-weixin'); const DIST_CONFIG = path.join(DIST_DIR, 'project.config.json'); /* ------------------------- 外部工具定位 ------------------------- */ // 扫描时要跳过的系统/无关目录 const SKIP_DIRS = /^(\$recycle\.bin|system volume information|windows|winsxs|node_modules|\.git|\.cache|programdata)$/i; const HX_TOOL = { envKey: 'HBUILDERX_CLI', label: 'HBuilderX 命令行 cli.exe', names: ['cli.exe'], subDirs: ['HBuilderX', 'HBuilderX\\HBuilderX'], dirPattern: /^hbuilderx/i, ok: function(file) { return /hbuilderx/i.test(file); }, hint: 'HBuilderX 安装/解压目录下的 cli.exe(如 X:\\HBuilderX\\cli.exe)', extra: '' }; const WX_TOOL = { envKey: 'WX_DEVTOOLS_CLI', label: '微信开发者工具命令行 cli.bat', names: ['cli.bat'], subDirs: ['微信web开发者工具', '微信开发者工具', 'Tencent\\微信web开发者工具', 'Tencent\\微信开发者工具'], dirPattern: /微信(web)?开发者工具/, hint: '微信开发者工具安装目录下的 cli.bat(如 X:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat)', extra: ' 3) 或加 --no-open,只编译不自动打开产物\n' }; function fail(msg) { console.error('[release] ' + msg); process.exit(1); } function uniq(list) { return list.filter(function(v, i) { return v && list.indexOf(v) === i; }); } function isFile(p) { try { return fs.statSync(p).isFile(); } catch (e) { return false; } } function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch (e) { return false; } } /** 本地固定磁盘(C:、D: …);拿不到时退回环境变量里的盘符(结果缓存,只探测一次) */ let DRIVES = null; function fixedDrives() { if (DRIVES) return DRIVES; const ps = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', "[System.IO.DriveInfo]::GetDrives() | Where-Object { $_.DriveType -eq 'Fixed' } | ForEach-Object { $_.Name }" ], { encoding: 'utf8', windowsHide: true }); const fromPs = (ps.stdout || '').split(/\r?\n/) .map(function(s) { return s.trim().replace(/\\$/, ''); }) .filter(function(s) { return /^[A-Za-z]:$/.test(s); }); const fromEnv = [process.env.SystemDrive, process.env.HOMEDRIVE]; DRIVES = uniq(fromPs.concat(fromEnv).map(function(d) { return (d || '').replace(/\\$/, ''); })); return DRIVES; } /** 在系统 PATH 中按文件名查找,ok() 用于排除同名无关程序 */ function findOnPath(names, ok) { const dirs = (process.env.PATH || '').split(path.delimiter); for (let i = 0; i < dirs.length; i++) { const dir = dirs[i].replace(/^"|"$/g, ''); if (!dir) continue; for (let j = 0; j < names.length; j++) { const file = path.join(dir, names[j]); if (isFile(file) && (!ok || ok(file))) return file; } } return ''; } /** 常见安装根目录:Program Files、用户目录、各磁盘根与常见软件目录 */ function baseDirs() { const bases = [ process.env.ProgramFiles, process.env['ProgramFiles(x86)'], process.env.LOCALAPPDATA, process.env.APPDATA, process.env.USERPROFILE, process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Programs'), process.env.USERPROFILE && path.join(process.env.USERPROFILE, 'Desktop'), process.env.USERPROFILE && path.join(process.env.USERPROFILE, 'Downloads') ]; fixedDrives().forEach(function(d) { bases.push(d + '\\', d + '\\Program Files', d + '\\Program Files (x86)', d + '\\soft', d + '\\software', d + '\\Programs', d + '\\develop'); }); return uniq(bases).filter(isDir); } /** 磁盘浅层扫描:找名字符合特征的目录(兜底手段) */ function scanPlausibleDirs(pattern) { const found = []; fixedDrives().forEach(function(root) { let level1 = []; try { level1 = fs.readdirSync(root + '\\', { withFileTypes: true }); } catch (e) { return; } level1.forEach(function(e1) { if (!e1.isDirectory() || SKIP_DIRS.test(e1.name)) return; const p1 = path.join(root + '\\', e1.name); if (pattern.test(e1.name)) { found.push(p1); return; } let level2 = []; try { level2 = fs.readdirSync(p1, { withFileTypes: true }); } catch (e) { return; } level2.forEach(function(e2) { if (e2.isDirectory() && pattern.test(e2.name)) found.push(path.join(p1, e2.name)); }); }); }); return found; } /** 定位工具:返回 { file, source },找不到返回 null */ function resolveTool(cfg) { // 1) 环境变量显式指定 const fromEnv = (process.env[cfg.envKey] || '').trim(); if (fromEnv) { if (isFile(fromEnv)) return { file: fromEnv, source: '环境变量 ' + cfg.envKey }; fail('环境变量 ' + cfg.envKey + ' 指向的文件不存在:' + fromEnv); } // 2) 系统 PATH const onPath = findOnPath(cfg.names, cfg.ok); if (onPath) return { file: onPath, source: '系统 PATH' }; // 3) 常见安装目录 + 磁盘浅层扫描 const dirs = baseDirs().concat(scanPlausibleDirs(cfg.dirPattern)); for (let i = 0; i < dirs.length; i++) { for (let j = 0; j < cfg.subDirs.length; j++) { for (let k = 0; k < cfg.names.length; k++) { const file = path.join(dirs[i], cfg.subDirs[j], cfg.names[k]); if (isFile(file)) return { file: file, source: '常见安装目录' }; } } } return null; } /** 找不到工具时的报错提示 */ function toolFail(cfg) { fail('未找到 ' + cfg.label + '\n' + ' 已查找:环境变量 ' + cfg.envKey + ' → 系统 PATH → 常见安装目录 → 磁盘浅层扫描\n' + ' 典型位置:' + cfg.hint + '\n' + ' 解决办法(任选其一):\n' + ' 1) 把安装目录加入 PATH:set PATH=%PATH%;<安装目录>\n' + ' 2) 用环境变量指定:set ' + cfg.envKey + '=\n' + cfg.extra); } /** 把工具所在目录加入本次进程 PATH,便于按名字直接调用 */ function putOnPath(file) { const dir = path.dirname(file); const cur = process.env.PATH || ''; if (cur.split(path.delimiter).indexOf(dir) < 0) process.env.PATH = dir + path.delimiter + cur; } /* ------------------------- 参数解析 ------------------------- */ const argv = process.argv.slice(2); const envName = (argv.find(function(a) { return a.charAt(0) !== '-'; }) || '').trim(); const noGen = argv.indexOf('--no-gen') > -1; const noOpen = argv.indexOf('--no-open') > -1; const dryRun = argv.indexOf('--dry-run') > -1; const OWN_ARGS = ['--no-gen', '--no-open', '--dry-run']; // 透传给 HBuilderX cli 的额外参数(保留 "参数 值" 成对关系) const passthrough = []; let envNameSkipped = false; argv.forEach(function(a) { if (OWN_ARGS.indexOf(a) > -1) return; if (!envNameSkipped && a === envName) { envNameSkipped = true; return; } passthrough.push(a); }); function readAppId(file) { try { return JSON.parse(fs.readFileSync(file, 'utf8')).appid || ''; } catch (e) { return ''; } } /** 回写 appid(正则替换,保留原格式) */ function writeAppId(file, appid) { const prev = fs.readFileSync(file, 'utf8'); const next = prev.replace(/"appid"(\s*:\s*)"[^"]*"/, function(m, sep) { return '"appid"' + sep + '"' + appid + '"'; }); if (next !== prev) fs.writeFileSync(file, next, 'utf8'); return readAppId(file); } function main() { const available = fs.existsSync(ENV_DIR) ? fs.readdirSync(ENV_DIR).filter(f => f.endsWith('.js') && f !== 'index.js').map(f => f.replace(/\.js$/, '')) : []; if (!envName) fail('请指定环境名,可选:' + available.join(' / ') + '\n 例:node scripts/publish.js sit'); if (available.indexOf(envName) < 0) fail('未找到 config/env/' + envName + '.js,可选:' + available.join(' / ')); const env = require(path.join(ENV_DIR, envName + '.js')); // 定位外部工具(不写死路径) console.log('[release] 查找 HBuilderX / 微信开发者工具…'); const hx = resolveTool(HX_TOOL); if (!hx) toolFail(HX_TOOL); const wx = noOpen ? null : resolveTool(WX_TOOL); if (!noOpen && !wx) toolFail(WX_TOOL); putOnPath(hx.file); if (wx) putOnPath(wx.file); // 目标编译产物由 manifest.json 决定,而 manifest.json 是生成物 if (noGen && !fs.existsSync(manifest.TARGET)) { fail('--no-gen 需要 manifest.json 已存在,请先运行:node scripts/set-env.js ' + envName); } console.log('[release] 环境 : ' + envName + '(' + env.ENV_NAME + ')'); console.log('[release] 小程序appid : ' + env.APPID); console.log('[release] 接口地址 : ' + env.HTTP_REQUEST_URL); console.log('[release] 产物目录 : ' + DIST_DIR); console.log('[release] HBuilderX : ' + hx.file + '(' + hx.source + ')'); if (wx) console.log('[release] 开发者工具 : ' + wx.file + '(' + wx.source + ')'); console.log('[release] manifest.json: ' + (noGen ? '不改动(--no-gen)' : '由 manifest.json.demo 生成(appid ' + env.APPID + ',生成物不入库)')); const cliArgs = ['publish', '--platform', 'mp-weixin', '--project', ROOT].concat(passthrough); // 路径含空格/中文,交给 cmd 解析更稳(--no-open 时没有微信开发者工具,不拼这条命令) const openCmd = wx ? '"' + wx.file + '" open --project "' + DIST_DIR + '"' : ''; if (dryRun) { console.log('[release] 0) 生成: manifest.json ← manifest.json.demo' + (noGen ? '(已指定 --no-gen,跳过)' : '')); console.log('[release] 1) 编译: "' + hx.file + '" ' + cliArgs.join(' ')); console.log('[release] 2) 产物 appid 回写为 ' + env.APPID + ' → ' + DIST_CONFIG); if (!noOpen) console.log('[release] 3) 打开: ' + openCmd); console.log('[release] --dry-run,未执行'); return; } // 1) 生成 manifest.json(HBuilderX 的编译输入,模板:manifest.json.demo) if (noGen) { console.log('[release] 跳过 manifest.json 生成(--no-gen)'); } else { manifest.generate(env); } // 2) 发行编译(产物:unpackage/dist/build/mp-weixin) console.log('[release] 开始发行编译,日志见 HBuilderX 控制台…'); const build = spawnSync(hx.file, cliArgs, { cwd: ROOT, stdio: 'inherit' }); if (build.error) fail('调用 cli.exe 失败:' + build.error.message); if (build.status !== 0) fail('cli.exe 退出码 ' + build.status + ',请查看 HBuilderX 控制台输出'); // 3) 校正产物 appid(正常应与 manifest 一致,这里兜底保证) if (!fs.existsSync(DIST_CONFIG)) { fail('发行产物缺少 project.config.json:' + DIST_CONFIG + '\n 请确认 HBuilderX 编译是否成功'); } const before = readAppId(DIST_CONFIG); const after = writeAppId(DIST_CONFIG, env.APPID); if (!after) fail('回写产物 appid 失败:' + DIST_CONFIG); console.log('[release] 产物 appid : ' + before + (before === after ? '(无需调整)' : ' → ' + after)); // 4) 用微信开发者工具打开产物(首次需在「设置-安全设置」中开启服务端口) if (!noOpen) { console.log('[release] 打开微信开发者工具…'); const open = spawnSync(openCmd, { cwd: ROOT, stdio: 'inherit', shell: true }); if (open.error || open.status !== 0) { console.log('[release] 自动打开失败(常见原因:未开启服务端口、微信开发者工具版本较旧)'); console.log('[release] 请手动打开微信开发者工具 → 导入项目 → 选择目录:' + DIST_DIR); } } console.log('[release] 完成。微信开发者工具中的项目 appid = ' + after + ',包内会自动使用 ' + env.HTTP_REQUEST_URL); } main();