Jacob
2026-10-12 486ac751755a33139c3fcbef3c73e3136ffcf5a5
scripts/publish.js
....@@ -12,7 +12,14 @@
1212 * 本脚本只负责让“编译期 appid”与目标环境一致 —— 微信开发者工具打开的产物 appid,
1313 * 就是 unpackage/dist/build/mp-weixin/project.config.json 里的 appid(构建时由 manifest 生成)。
1414 *
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(便于按名字直接调用),找不到则报错并给出解决办法。
1623 */
1724 const { spawnSync } = require('child_process');
1825 const fs = require('fs');
....@@ -23,9 +30,162 @@
2330 const DIST_DIR = path.join(ROOT, 'unpackage', 'dist', 'build', 'mp-weixin');
2431 const DIST_CONFIG = path.join(DIST_DIR, 'project.config.json');
2532
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
+/* ------------------------- 参数解析 ------------------------- */
29189
30190 const argv = process.argv.slice(2);
31191 const envName = (argv.find(function(a) { return a.charAt(0) !== '-'; }) || '').trim();
....@@ -45,11 +205,6 @@
45205 }
46206 passthrough.push(a);
47207 });
48
-
49
-function fail(msg) {
50
- console.error('[release] ' + msg);
51
- process.exit(1);
52
-}
53208
54209 function readAppId(file) {
55210 try {
....@@ -79,27 +234,29 @@
79234
80235 const env = require(path.join(ENV_DIR, envName + '.js'));
81236
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);
90245
91246 console.log('[release] 环境 : ' + envName + '(' + env.ENV_NAME + ')');
92247 console.log('[release] 小程序appid : ' + env.APPID);
93248 console.log('[release] 接口地址 : ' + env.HTTP_REQUEST_URL);
94249 console.log('[release] 产物目录 : ' + DIST_DIR);
250
+ console.log('[release] HBuilderX : ' + hx.file + '(' + hx.source + ')');
251
+ if (wx) console.log('[release] 开发者工具 : ' + wx.file + '(' + wx.source + ')');
95252 console.log('[release] manifest.json: ' + (syncManifest ? '同步为 ' + envName + ' 的 appid(会产生 git 改动)' : '保持不动(仓库维持基线,无 git 改动)'));
96253
97254 const cliArgs = ['publish', '--platform', 'mp-weixin', '--project', ROOT].concat(passthrough);
98255 // 路径含空格/中文,交给 cmd 解析更稳
99
- const openCmd = '"' + WX_CLI + '" open --project "' + DIST_DIR + '"';
256
+ const openCmd = '"' + wx.file + '" open --project "' + DIST_DIR + '"';
100257
101258 if (dryRun) {
102
- console.log('[release] 1) 编译: "' + HX_CLI + '" ' + cliArgs.join(' '));
259
+ console.log('[release] 1) 编译: "' + hx.file + '" ' + cliArgs.join(' '));
103260 console.log('[release] 2) 产物 appid 回写为 ' + env.APPID + ' → ' + DIST_CONFIG);
104261 if (!noOpen) console.log('[release] 3) 打开: ' + openCmd);
105262 console.log('[release] --dry-run,未执行');
....@@ -115,7 +272,7 @@
115272
116273 // 2) 发行编译(产物:unpackage/dist/build/mp-weixin)
117274 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' });
119276 if (build.error) fail('调用 cli.exe 失败:' + build.error.message);
120277 if (build.status !== 0) fail('cli.exe 退出码 ' + build.status + ',请查看 HBuilderX 控制台输出');
121278