Jacob
1 天以前 76005787a10f1cf2e26f1b0c1672836b7d633ed2
scripts/publish.js
....@@ -1,40 +1,203 @@
11 /**
2
- * HBuilderX CLI 命令行发布:按环境发布微信小程序
2
+ * 命令行发行(不上传)+ 自动用微信开发者工具打开产物
33 *
44 * 用法:
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 # 只打印将要执行的命令
1010 *
11
- * 环境相关的值统一取自 config/env/<env>.js(唯一数据源):
12
- * - 接口/埋点等运行时参数:包内自动按小程序 appid 识别,无需在此处理
13
- * - APPID:作为 --appid 传给微信 CI 上传
11
+ * 流程:从 manifest.json.demo 生成 manifest.json(按目标环境注入 appid)→ 发行编译 →
12
+ * 校正产物 appid → 用微信开发者工具打开产物
1413 *
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(便于按名字直接调用),找不到则报错并给出解决办法。
1825 */
1926 const { spawnSync } = require('child_process');
2027 const fs = require('fs');
2128 const path = require('path');
29
+const manifest = require('./manifest');
2230
2331 const ROOT = path.resolve(__dirname, '..');
2432 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');
2535
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
+/* ------------------------- 参数解析 ------------------------- */
28192
29193 const argv = process.argv.slice(2);
30194 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;
32197 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 的额外参数(保留 "参数 值" 成对关系)
38201 const passthrough = [];
39202 let envNameSkipped = false;
40203 argv.forEach(function(a) {
....@@ -46,24 +209,22 @@
46209 passthrough.push(a);
47210 });
48211
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) {
60213 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 || '';
64215 } catch (e) {
65
- return '1.0.0';
216
+ return '';
66217 }
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);
67228 }
68229
69230 function main() {
....@@ -76,58 +237,76 @@
76237
77238 const env = require(path.join(ENV_DIR, envName + '.js'));
78239
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);
83252 }
84253
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 + ',生成物不入库)'));
87263
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 解析更稳(--no-open 时没有微信开发者工具,不拼这条命令)
266
+ const openCmd = wx ? '"' + wx.file + '" open --project "' + DIST_DIR + '"' : '';
111267
112268 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,未执行');
114274 return;
115275 }
116276
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);
122282 }
123283
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 控制台输出');
126289
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));
129298
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);
131310 }
132311
133312 main();