/** * manifest.json 生成器 * * manifest.json 是 HBuilderX 的编译输入,但内含环境相关 appid,不适合直接提交; * 因此模板入库、生成物忽略: * * manifest.json.demo(入库,模板) ──注入环境 appid──▶ manifest.json(.gitignore) * * appid 数据源唯一:config/env/.js 的 UNI_APPID(DCloud 标识)、APPID(微信小程序 appid) * 说明:模板里 mp-weixin 的 appid 允许写成 null 或不存在,生成时会自动写入/补齐。 */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const TEMPLATE = path.join(ROOT, 'manifest.json.demo'); const TARGET = path.join(ROOT, 'manifest.json'); function fail(msg) { console.error('[manifest] ' + msg); process.exit(1); } /** 只替换第一处 "appid" : <字符串或 null>,保留原有的空格/缩进风格 */ function replaceAppId(text, value) { let done = false; const result = text.replace(/"appid"(\s*:\s*)(?:"[^"]*"|null)/, function(match, sep) { done = true; return '"appid"' + sep + '"' + value + '"'; }); return { text: result, done: done }; } /** 注入两个 appid:顶层(DCloud 标识)+ mp-weixin 块内(微信小程序 appid) */ function injectAppIds(text, env) { // 1) 顶层 appid(第一处出现即为顶层) const top = replaceAppId(text, env.UNI_APPID); if (!top.done) fail('模板中未找到顶层 "appid" 字段:' + path.relative(ROOT, TEMPLATE)); text = top.text; // 2) mp-weixin 下的 appid,只在该区块内替换第一处 const idx = text.indexOf('"mp-weixin"'); if (idx < 0) fail('模板中未找到 "mp-weixin" 配置块:' + path.relative(ROOT, TEMPLATE)); const head = text.slice(0, idx); const body = text.slice(idx); const scoped = replaceAppId(body, env.APPID); if (scoped.done) return head + scoped.text; // 3) 字段被删掉时,按同级子项缩进补一行 const m = body.match(/^("mp-weixin"\s*:\s*\{)(\r?\n)([ \t]*)/); if (!m) fail('模板的 "mp-weixin" 块格式异常,无法写入 appid'); const nl = m[2]; const indent = m[3]; return head + m[1] + nl + indent + '"appid" : "' + env.APPID + '",' + nl + indent + body.slice(m[0].length); } /** 统计「除 appid 行以外」的差异(按行内容比较,不受行号位移影响) */ function otherDiff(a, b) { function count(text) { const m = new Map(); text.split(/\r?\n/).forEach(function(line) { const l = line.replace(/\s+$/, ''); if (/^\s*"appid"\s*:/.test(l.trim())) return; m.set(l, (m.get(l) || 0) + 1); }); return m; } const ma = count(a); const mb = count(b); const keys = new Set(Array.from(ma.keys()).concat(Array.from(mb.keys()))); let diff = 0; const sample = []; keys.forEach(function(k) { const d = Math.abs((ma.get(k) || 0) - (mb.get(k) || 0)); if (!d) return; diff += d; if (sample.length < 3 && k.trim()) sample.push(k.trim().slice(0, 40)); }); return { count: diff, sample: sample }; } /** * 按环境生成 manifest.json * @param {object} env config/env/.js 导出的配置 * @returns {{ file: string, changed: boolean }} */ function generate(env) { if (!fs.existsSync(TEMPLATE)) { fail('未找到模板 ' + path.relative(ROOT, TEMPLATE) + '\n 请把 manifest.json 的模板保存为 manifest.json.demo(需入库)后再生成'); } const rendered = injectAppIds(fs.readFileSync(TEMPLATE, 'utf8'), env); const exists = fs.existsSync(TARGET); if (exists) { const prev = fs.readFileSync(TARGET, 'utf8'); if (prev === rendered) { console.log('[manifest] manifest.json 已是目标内容,无需重写'); return { file: TARGET, changed: false }; } const diffs = otherDiff(prev, rendered); if (diffs.count) { console.log('[manifest] 注意:现有 manifest.json 有 ' + diffs.count + ' 行内容与模板不同,本次会被模板覆盖' + (diffs.sample.length ? '(如 ' + diffs.sample.join(' | ') + ')' : '')); console.log('[manifest] 需要长期保留的改动请写进 manifest.json.demo'); } } fs.writeFileSync(TARGET, rendered, 'utf8'); console.log('[manifest] 已由 manifest.json.demo 生成 manifest.json' + '(DCloud ' + env.UNI_APPID + ' / 小程序 ' + env.APPID + ')'); return { file: TARGET, changed: true }; } module.exports = { TEMPLATE: TEMPLATE, TARGET: TARGET, generate: generate, injectAppIds: injectAppIds };