Jacob
2026-10-12 486450d6a4a0cc1b7de4c053cf561e54b7aafc85
feat: manifest.json 改为由 manifest.json.demo 生成
1个文件已添加
4个文件已修改
247 ■■■■■ 已修改文件
scripts/manifest.js 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/publish.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/set-env.js 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
switch-prod.bat 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
switch-sit.bat 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/manifest.js
....@@ -0,0 +1,121 @@
1
+/**
2
+ * manifest.json 生成器
3
+ *
4
+ * manifest.json 是 HBuilderX 的编译输入,但内含环境相关 appid,不适合直接提交;
5
+ * 因此模板入库、生成物忽略:
6
+ *
7
+ * manifest.json.demo(入库,模板) ──注入环境 appid──▶ manifest.json(.gitignore)
8
+ *
9
+ * appid 数据源唯一:config/env/<env>.js 的 UNI_APPID(DCloud 标识)、APPID(微信小程序 appid)
10
+ * 说明:模板里 mp-weixin 的 appid 允许写成 null 或不存在,生成时会自动写入/补齐。
11
+ */
12
+const fs = require('fs');
13
+const path = require('path');
14
+
15
+const ROOT = path.resolve(__dirname, '..');
16
+const TEMPLATE = path.join(ROOT, 'manifest.json.demo');
17
+const TARGET = path.join(ROOT, 'manifest.json');
18
+
19
+function fail(msg) {
20
+ console.error('[manifest] ' + msg);
21
+ process.exit(1);
22
+}
23
+
24
+/** 只替换第一处 "appid" : <字符串或 null>,保留原有的空格/缩进风格 */
25
+function replaceAppId(text, value) {
26
+ let done = false;
27
+ const result = text.replace(/"appid"(\s*:\s*)(?:"[^"]*"|null)/, function(match, sep) {
28
+ done = true;
29
+ return '"appid"' + sep + '"' + value + '"';
30
+ });
31
+ return { text: result, done: done };
32
+}
33
+
34
+/** 注入两个 appid:顶层(DCloud 标识)+ mp-weixin 块内(微信小程序 appid) */
35
+function injectAppIds(text, env) {
36
+ // 1) 顶层 appid(第一处出现即为顶层)
37
+ const top = replaceAppId(text, env.UNI_APPID);
38
+ if (!top.done) fail('模板中未找到顶层 "appid" 字段:' + path.relative(ROOT, TEMPLATE));
39
+ text = top.text;
40
+
41
+ // 2) mp-weixin 下的 appid,只在该区块内替换第一处
42
+ const idx = text.indexOf('"mp-weixin"');
43
+ if (idx < 0) fail('模板中未找到 "mp-weixin" 配置块:' + path.relative(ROOT, TEMPLATE));
44
+ const head = text.slice(0, idx);
45
+ const body = text.slice(idx);
46
+ const scoped = replaceAppId(body, env.APPID);
47
+ if (scoped.done) return head + scoped.text;
48
+
49
+ // 3) 字段被删掉时,按同级子项缩进补一行
50
+ const m = body.match(/^("mp-weixin"\s*:\s*\{)(\r?\n)([ \t]*)/);
51
+ if (!m) fail('模板的 "mp-weixin" 块格式异常,无法写入 appid');
52
+ const nl = m[2];
53
+ const indent = m[3];
54
+ return head + m[1] + nl + indent + '"appid" : "' + env.APPID + '",' + nl + indent + body.slice(m[0].length);
55
+}
56
+
57
+/** 统计「除 appid 行以外」的差异(按行内容比较,不受行号位移影响) */
58
+function otherDiff(a, b) {
59
+ function count(text) {
60
+ const m = new Map();
61
+ text.split(/\r?\n/).forEach(function(line) {
62
+ const l = line.replace(/\s+$/, '');
63
+ if (/^\s*"appid"\s*:/.test(l.trim())) return;
64
+ m.set(l, (m.get(l) || 0) + 1);
65
+ });
66
+ return m;
67
+ }
68
+ const ma = count(a);
69
+ const mb = count(b);
70
+ const keys = new Set(Array.from(ma.keys()).concat(Array.from(mb.keys())));
71
+ let diff = 0;
72
+ const sample = [];
73
+ keys.forEach(function(k) {
74
+ const d = Math.abs((ma.get(k) || 0) - (mb.get(k) || 0));
75
+ if (!d) return;
76
+ diff += d;
77
+ if (sample.length < 3 && k.trim()) sample.push(k.trim().slice(0, 40));
78
+ });
79
+ return { count: diff, sample: sample };
80
+}
81
+
82
+/**
83
+ * 按环境生成 manifest.json
84
+ * @param {object} env config/env/<env>.js 导出的配置
85
+ * @returns {{ file: string, changed: boolean }}
86
+ */
87
+function generate(env) {
88
+ if (!fs.existsSync(TEMPLATE)) {
89
+ fail('未找到模板 ' + path.relative(ROOT, TEMPLATE) +
90
+ '\n 请把 manifest.json 的模板保存为 manifest.json.demo(需入库)后再生成');
91
+ }
92
+
93
+ const rendered = injectAppIds(fs.readFileSync(TEMPLATE, 'utf8'), env);
94
+ const exists = fs.existsSync(TARGET);
95
+
96
+ if (exists) {
97
+ const prev = fs.readFileSync(TARGET, 'utf8');
98
+ if (prev === rendered) {
99
+ console.log('[manifest] manifest.json 已是目标内容,无需重写');
100
+ return { file: TARGET, changed: false };
101
+ }
102
+ const diffs = otherDiff(prev, rendered);
103
+ if (diffs.count) {
104
+ console.log('[manifest] 注意:现有 manifest.json 有 ' + diffs.count + ' 行内容与模板不同,本次会被模板覆盖' +
105
+ (diffs.sample.length ? '(如 ' + diffs.sample.join(' | ') + ')' : ''));
106
+ console.log('[manifest] 需要长期保留的改动请写进 manifest.json.demo');
107
+ }
108
+ }
109
+
110
+ fs.writeFileSync(TARGET, rendered, 'utf8');
111
+ console.log('[manifest] 已由 manifest.json.demo 生成 manifest.json' +
112
+ '(DCloud ' + env.UNI_APPID + ' / 小程序 ' + env.APPID + ')');
113
+ return { file: TARGET, changed: true };
114
+}
115
+
116
+module.exports = {
117
+ TEMPLATE: TEMPLATE,
118
+ TARGET: TARGET,
119
+ generate: generate,
120
+ injectAppIds: injectAppIds
121
+};
scripts/publish.js
....@@ -2,17 +2,19 @@
22 * 命令行发行(不上传)+ 自动用微信开发者工具打开产物
33 *
44 * 用法:
5
- * node scripts/publish.js sit # 发行 SIT 版本并打开微信开发者工具
6
- * node scripts/publish.js prod # 发行 PROD 版本并打开微信开发者工具
7
- * node scripts/publish.js sit --sync # 同时把 appid 同步进 manifest.json(默认不动 manifest)
8
- * node scripts/publish.js sit --no-open # 只编译,不打开微信开发者工具
9
- * node scripts/publish.js sit --dry-run # 只打印将要执行的命令
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”与目标环境一致 —— 微信开发者工具打开的产物 appid,
13
- * 就是 unpackage/dist/build/mp-weixin/project.config.json 里的 appid(构建时由 manifest 生成)。
11
+ * 流程:从 manifest.json.demo 生成 manifest.json(按目标环境注入 appid)→ 发行编译 →
12
+ * 校正产物 appid → 用微信开发者工具打开产物
1413 *
15
- * 默认策略:不动 manifest.json(仓库保持基线,git 无噪音),只回写未跟踪的产物文件。
14
+ * 环境值统一取自 config/env/<env>.js(唯一数据源),接口地址等运行时参数由包内按 appid 自动识别;
15
+ * manifest.json 是生成物(已 .gitignore),因此 git 永远没有环境噪音。
16
+ * 微信开发者工具打开的产物 appid = unpackage/dist/build/mp-weixin/project.config.json 的 appid
17
+ * (构建时由 manifest 生成,这里再兜底校正一次)。
1618 *
1719 * 外部工具(HBuilderX cli.exe、微信开发者工具 cli.bat)不写死绝对路径,按顺序自动查找:
1820 * 1) 环境变量 HBUILDERX_CLI / WX_DEVTOOLS_CLI
....@@ -24,6 +26,7 @@
2426 const { spawnSync } = require('child_process');
2527 const fs = require('fs');
2628 const path = require('path');
29
+const manifest = require('./manifest');
2730
2831 const ROOT = path.resolve(__dirname, '..');
2932 const ENV_DIR = path.join(ROOT, 'config', 'env');
....@@ -189,11 +192,11 @@
189192
190193 const argv = process.argv.slice(2);
191194 const envName = (argv.find(function(a) { return a.charAt(0) !== '-'; }) || '').trim();
192
-const syncManifest = argv.indexOf('--sync') > -1;
195
+const noGen = argv.indexOf('--no-gen') > -1;
193196 const noOpen = argv.indexOf('--no-open') > -1;
194197 const dryRun = argv.indexOf('--dry-run') > -1;
195198
196
-const OWN_ARGS = ['--sync', '--no-open', '--dry-run'];
199
+const OWN_ARGS = ['--no-gen', '--no-open', '--dry-run'];
197200 // 透传给 HBuilderX cli 的额外参数(保留 "参数 值" 成对关系)
198201 const passthrough = [];
199202 let envNameSkipped = false;
....@@ -243,19 +246,27 @@
243246 putOnPath(hx.file);
244247 if (wx) putOnPath(wx.file);
245248
249
+ // 目标编译产物由 manifest.json 决定,而 manifest.json 是生成物
250
+ if (noGen && !fs.existsSync(manifest.TARGET)) {
251
+ fail('--no-gen 需要 manifest.json 已存在,请先运行:node scripts/set-env.js ' + envName);
252
+ }
253
+
246254 console.log('[release] 环境 : ' + envName + '(' + env.ENV_NAME + ')');
247255 console.log('[release] 小程序appid : ' + env.APPID);
248256 console.log('[release] 接口地址 : ' + env.HTTP_REQUEST_URL);
249257 console.log('[release] 产物目录 : ' + DIST_DIR);
250258 console.log('[release] HBuilderX : ' + hx.file + '(' + hx.source + ')');
251259 if (wx) console.log('[release] 开发者工具 : ' + wx.file + '(' + wx.source + ')');
252
- console.log('[release] manifest.json: ' + (syncManifest ? '同步为 ' + envName + ' 的 appid(会产生 git 改动)' : '保持不动(仓库维持基线,无 git 改动)'));
260
+ console.log('[release] manifest.json: ' + (noGen
261
+ ? '不改动(--no-gen)'
262
+ : '由 manifest.json.demo 生成(appid ' + env.APPID + ',生成物不入库)'));
253263
254264 const cliArgs = ['publish', '--platform', 'mp-weixin', '--project', ROOT].concat(passthrough);
255265 // 路径含空格/中文,交给 cmd 解析更稳
256266 const openCmd = '"' + wx.file + '" open --project "' + DIST_DIR + '"';
257267
258268 if (dryRun) {
269
+ console.log('[release] 0) 生成: manifest.json ← manifest.json.demo' + (noGen ? '(已指定 --no-gen,跳过)' : ''));
259270 console.log('[release] 1) 编译: "' + hx.file + '" ' + cliArgs.join(' '));
260271 console.log('[release] 2) 产物 appid 回写为 ' + env.APPID + ' → ' + DIST_CONFIG);
261272 if (!noOpen) console.log('[release] 3) 打开: ' + openCmd);
....@@ -263,11 +274,11 @@
263274 return;
264275 }
265276
266
- // 1) 可选:同步 manifest.json(会让被跟踪文件产生改动,默认不做)
267
- if (syncManifest) {
268
- console.log('[release] 同步 manifest.json 的 appid…');
269
- const sync = spawnSync(process.execPath, [path.join(__dirname, 'set-env.js'), envName], { cwd: ROOT, stdio: 'inherit' });
270
- 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);
271282 }
272283
273284 // 2) 发行编译(产物:unpackage/dist/build/mp-weixin)
....@@ -276,7 +287,7 @@
276287 if (build.error) fail('调用 cli.exe 失败:' + build.error.message);
277288 if (build.status !== 0) fail('cli.exe 退出码 ' + build.status + ',请查看 HBuilderX 控制台输出');
278289
279
- // 3) 校正产物 appid(--sync 时通常已一致,这里兜底保证)
290
+ // 3) 校正产物 appid(正常应与 manifest 一致,这里兜底保证)
280291 if (!fs.existsSync(DIST_CONFIG)) {
281292 fail('发行产物缺少 project.config.json:' + DIST_CONFIG + '\n 请确认 HBuilderX 编译是否成功');
282293 }
scripts/set-env.js
....@@ -1,78 +1,31 @@
11 /**
2
- * 环境切换脚本:把 config/env/<env>.js 里的 appid 同步到编译期配置文件
2
+ * 环境切换:按 config/env/<env>.js 从模板生成 manifest.json
33 *
44 * 用法:
55 * npm run env:sit 等价于 node scripts/set-env.js sit
66 * npm run env:prod 等价于 node scripts/set-env.js prod
77 *
88 * 说明:
9
- * appid 是编译期值(HBuilderX 的编译输入就是 manifest.json),无法像接口地址那样
10
- * 运行时切换,所以这里做“一次命令写回”。改完必须重新编译一次,产物里的
11
- * project.config.json 才会更新。
9
+ * manifest.json 是 HBuilderX 的编译输入(appid 属于编译期值,无法像接口地址那样运行时切换),
10
+ * 但它含环境相关 appid,不适合直接提交,所以:
11
+ * manifest.json.demo(入库模板) --生成--> manifest.json(已 .gitignore)
12
+ * 生成后必须重新编译一次,并在微信开发者工具中重新打开项目(appid 变更需重开)。
1213 *
13
- * 日常推荐:mp-weixin.appid 保持 null,只让发行脚本回写产物 appid(git 零噪音)。
14
- * 本脚本用于需要在 HBuilderX 里直接运行调试、或要出 App 包(顶层 appid)的场景。
14
+ * 如需在 HBuilderX 里直接编译/运行调试,先跑一次本脚本;
15
+ * 走命令行发行则直接用 scripts/publish.js(内部会自动生成)。
1516 *
16
- * 唯一数据源:config/env/<env>.js 中的 UNI_APPID(DCloud 标识)、APPID(小程序 appid)
17
+ * appid 数据源唯一:config/env/<env>.js 中的 UNI_APPID(DCloud 标识)、APPID(小程序 appid)
1718 */
1819 const fs = require('fs');
1920 const path = require('path');
21
+const manifest = require('./manifest');
2022
2123 const ROOT = path.resolve(__dirname, '..');
2224 const ENV_DIR = path.join(ROOT, 'config', 'env');
2325
24
-// 根目录 project.config.json 不参与 mp-weixin 编译且已不入库,这里只维护 manifest.json
25
-const files = {
26
- manifest: path.join(ROOT, 'manifest.json')
27
-};
28
-
2926 function fail(msg) {
3027 console.error('[set-env] ' + msg);
3128 process.exit(1);
32
-}
33
-
34
-/** 只替换第一处 "appid" : <字符串或 null>,保留原有空格/缩进风格 */
35
-function replaceAppId(text, value) {
36
- let done = false;
37
- const result = text.replace(/"appid"(\s*:\s*)(?:"[^"]*"|null)/, function(match, sep) {
38
- done = true;
39
- return '"appid"' + sep + '"' + value + '"';
40
- });
41
- return { text: result, done: done };
42
-}
43
-
44
-/** 替换 manifest.json 顶层 appid(第一处出现即为顶层) */
45
-function patchManifest(text, env) {
46
- // 1) 顶层 appid(DCloud 应用标识)
47
- const top = replaceAppId(text, env.UNI_APPID);
48
- if (!top.done) fail('manifest.json 中未找到顶层 "appid" 字段');
49
- text = top.text;
50
-
51
- // 2) mp-weixin 下的 appid(微信小程序 appid),只在该区块内替换第一处
52
- const idx = text.indexOf('"mp-weixin"');
53
- if (idx < 0) fail('manifest.json 中未找到 "mp-weixin" 配置块');
54
- const head = text.slice(0, idx);
55
- const body = text.slice(idx);
56
- const scoped = replaceAppId(body, env.APPID);
57
- if (scoped.done) return head + scoped.text;
58
-
59
- // 3) 字段被删掉时,按同级子项缩进补一行
60
- const m = body.match(/^("mp-weixin"\s*:\s*\{)(\r?\n)([ \t]*)/);
61
- if (!m) fail('manifest.json 的 "mp-weixin" 块格式异常,无法写入 appid');
62
- const nl = m[2];
63
- const indent = m[3];
64
- return head + m[1] + nl + indent + '"appid" : "' + env.APPID + '",' + nl + indent + body.slice(m[0].length);
65
-}
66
-
67
-function writeIfChanged(file, next) {
68
- const prev = fs.readFileSync(file, 'utf8');
69
- if (prev === next) {
70
- console.log(' - ' + path.relative(ROOT, file) + ' 已是目标值,无需改动');
71
- return false;
72
- }
73
- fs.writeFileSync(file, next, 'utf8');
74
- console.log(' √ ' + path.relative(ROOT, file) + ' 已更新');
75
- return true;
7629 }
7730
7831 function main() {
....@@ -98,11 +51,11 @@
9851 console.log(' 小程序 appid : ' + env.APPID);
9952 console.log(' 接口地址 : ' + env.HTTP_REQUEST_URL);
10053
101
- const changed = writeIfChanged(files.manifest, patchManifest(fs.readFileSync(files.manifest, 'utf8'), env));
54
+ const res = manifest.generate(env);
10255
103
- console.log(changed
56
+ console.log(res.changed
10457 ? '[set-env] 完成,请在 HBuilderX 中重新编译一次,并在微信开发者工具中重新打开项目'
105
- : '[set-env] 无需改动,当前已是 ' + envName + ' 环境配置');
58
+ : '[set-env] 无需改动,当前 manifest.json 已是 ' + envName + ' 环境');
10659 }
10760
10861 main();
switch-prod.bat
....@@ -7,8 +7,8 @@
77 echo 接口 : https://acmall.bluedeer.com.cn/crmeb/front
88 echo ============================================
99 echo.
10
-echo 说明:改写 manifest.json / project.config.json 的编译期 appid
11
-echo (接口地址是运行时按 appid 自动识别的,无需改动)
10
+echo 说明:按 config/env/prod.js,从 manifest.json.demo 生成 manifest.json
11
+echo (manifest.json 是生成物、已不入库;接口地址运行时按 appid 自动识别)
1212 echo.
1313 node "scripts\set-env.js" prod
1414 echo.
switch-sit.bat
....@@ -7,8 +7,8 @@
77 echo 接口 : https://sit.tohours.com/crmeb/front
88 echo ============================================
99 echo.
10
-echo 说明:改写 manifest.json / project.config.json 的编译期 appid
11
-echo (接口地址是运行时按 appid 自动识别的,无需改动)
10
+echo 说明:按 config/env/sit.js,从 manifest.json.demo 生成 manifest.json
11
+echo (manifest.json 是生成物、已不入库;接口地址运行时按 appid 自动识别)
1212 echo.
1313 node "scripts\set-env.js" sit
1414 echo.