/** * Build-time: 给 pages.json 里所有 page.style 块补上 navigationBarTitleText 字段 * (如果不缺则跳过) * * 设计原则: 与 inject-placeholder.js 完全一致 * - 不调用 JSON.parse / JSON.stringify (它们会丢失注释和预处理指令) * - 不剥离 // 注释 * - 不剥离 // #ifdef / // #ifndef / // #endif 等 uni-app 预处理指令 * - 只对每个 page 的 "style": { ... } 块做局部修改 * * 注入策略: * 1. 跟踪 subPackage root + page path * 2. 对每个 "style": { ... } 块: * a) body 内已存在 "navigationBarTitleText" → 跳过 * b) 不存在 → 找到 body 内"应当插入"的位置: * - 跳过开头的 // #ifdef / // #ifndef 指令和空行 * - 在 body 内, 在第一个真正生效(非被 #ifdef 屏蔽)的字段行之前 * - 或在第一个 // #ifdef APP-PLUS 之前 (确保所有平台生效) * c) 插入 "navigationBarTitleText": "" 并正确处理逗号: * - 插入行自身根据"后面是否还有字段"决定是否加尾逗号 * - 若插入位置在现有字段前, 给该字段前一行加逗号; 把原字段自身的逗号去掉 */ const path = require('path'); const fs = require('fs'); const PROJECT_ROOT = path.join(__dirname, '..'); const PAGES_JSON = path.join(PROJECT_ROOT, 'pages.json'); function netBraces(line) { let d = 0; let inStr = false; let esc = false; let i = 0; while (i < line.length) { const c = line[i]; if (inStr) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; } else { if (c === '/' && line[i + 1] === '/') break; if (c === '"') inStr = true; else if (c === '{') d++; else if (c === '}') d--; } i++; } return d; } function isMeaningfulFieldLine(line) { return /^\s*"[^"]+"\s*:/.test(line); } function isPreprocOpen(line) { return /^\s*\/\/\s*#(ifdef|ifndef)\b/.test(line); } function isPreprocClose(line) { return /^\s*\/\/\s*#endif\b/.test(line); } /** * 在 body 中找"插入位置". 规则: * - 维护 ifdefDepth (类似栈深度) * - 在 ifdefDepth==0 且遇到第一个有意义字段行之前, 把 insertAt 记为当前位置(若还没记) * - 若遇到第一个 // #ifdef 块且还没记 insertAt, 把 insertAt 记为该 #ifdef 行索引 * (这样插入位置在 #ifdef 之前, 保证所有平台生效) * 返回 insertAt (body 内的索引) */ function findInsertIdx(bodyLines) { let insertAt = -1; let ifdefDepth = 0; for (let k = 0; k < bodyLines.length; k++) { const t = bodyLines[k]; if (isPreprocOpen(t)) { if (insertAt === -1) insertAt = k; // 在 #ifdef 之前插入 ifdefDepth++; continue; } if (isPreprocClose(t)) { ifdefDepth = Math.max(0, ifdefDepth - 1); continue; } if (ifdefDepth > 0) continue; // 在 #ifdef 块内跳过 if (isMeaningfulFieldLine(t)) { if (insertAt === -1) insertAt = k; break; } } if (insertAt === -1) insertAt = bodyLines.length; // 全部空, 末尾追加 return insertAt; } /** * 给字段行加前导逗号 (例如变成 `,"xxx": "yyy"`). * 如果行已经有前导逗号则返回原样. 处理行尾 // 注释. */ function addLeadingComma(line) { const cmtMatch = line.match(/^(\s*)(\/\/.*)$/); if (cmtMatch) return line; // 纯注释行不动 if (/^\s*,/.test(line)) return line; return line.replace(/^(\s*)/, '$1,'); } /** * 处理单个 style 块. * * 关键约束: 兼容 uni-app 的 pages.json 条件编译预处理 (// #ifdef APP-PLUS ... // #endif). * 编译器按平台把整个条件块剥除, 因此: * - 块被保留的平台: 必须满足 JSON 语法 (逗号规则) * - 块被剥除的平台: 不能留下孤儿逗号 * * 业界已验证的安全模式 (CSDN/blog.csdn.net/dujyong): * 1) 若 style 末尾只有 // #ifdef ... // #endif 这种"条件块作为最后一项": * 块内部首字段前加逗号; 上一行不加尾逗号. 例: * "enablePullDownRefresh": true * // #ifdef APP-PLUS * ,"navigationStyle": "custom" * // #endif * * 2) 若 style 内除了条件块外, 后面还跟着其它字段: * 把 // #ifdef 块整体挪到 style 最末尾, 然后用模式 1 插入 title. * 这样 title 总是放在条件块之前, 且条件块始终是 style 的最后一项. * * 输出: * "navigationBarTitleText": "" (无尾逗号) * ,"enablePullDownRefresh": true (首字段, 加前导逗号; 仅在 insertAt==0 时加) * ... * ,"": ... (末字段前加前导逗号, 让块剥除后逗号跟着消失) * // #ifdef APP-PLUS * ,"navigationStyle": "custom" (条件块内首字段前导逗号) * // #endif * * 通用规则: 给所有"在 navigationBarTitleText 之后的字段行"加前导逗号. * navigationBarTitleText 自身无尾逗号; 块剥除时所有前导逗号随对应字段一起消失. * * 这等价于: 在每一行 (除首行) 的字段前加逗号, 即 trailing comma 的反向版本. */ function processStyleBlock(openLine, bodyLines, closeLine, innerIndent) { if (bodyLines.some(l => /^\s*"navigationBarTitleText"\s*:/.test(l))) { return { lines: [openLine, ...bodyLines, closeLine], action: 'skip' }; } const insertAt = findInsertIdx(bodyLines); const newBody = bodyLines.slice(); // 标题行: 无尾逗号 (逗号挂在下一个字段上) const titleLine = `${innerIndent}"navigationBarTitleText": ""`; // 给 insertAt 之后所有"未被条件块屏蔽"的字段行加前导逗号; // insertAt 之前的所有字段保留原样 (它们按 JSONC 规范自带逗号). for (let k = insertAt; k < newBody.length; k++) { if (isMeaningfulFieldLine(newBody[k])) { newBody[k] = addLeadingComma(newBody[k]); } } newBody.splice(insertAt, 0, titleLine); return { lines: [openLine, ...newBody, closeLine], action: 'inject' }; } function run() { let raw; try { raw = fs.readFileSync(PAGES_JSON, 'utf-8'); } catch (e) { console.error('[InjectNavTitle] ❌ 读取 pages.json 失败:', e.message); return false; } const EOL = raw.includes('\r\n') ? '\r\n' : '\n'; const lines = raw.split(/\r?\n/); const out = []; let i = 0; let injected = 0, skipped = 0; while (i < lines.length) { const line = lines[i]; const styleMatch = line.match(/^(\s*)"style"\s*:\s*\{\s*,?\s*$/); if (!styleMatch) { out.push(line); i++; continue; } const styleIndent = styleMatch[1]; const innerIndent = styleIndent + '\t'; const blockStart = i; let depth = 1; i++; while (i < lines.length && depth > 0) { depth += netBraces(lines[i]); i++; } const blockEnd = i; if (depth !== 0) { for (let j = blockStart; j < blockEnd; j++) out.push(lines[j]); continue; } const blockLines = lines.slice(blockStart, blockEnd); const closeLine = blockLines[blockLines.length - 1]; const bodyLines = blockLines.slice(1, -1); const result = processStyleBlock(blockLines[0], bodyLines, closeLine, innerIndent); for (const l of result.lines) out.push(l); if (result.action === 'inject') injected++; else if (result.action === 'skip') skipped++; } const outText = out.join(EOL); if (outText !== raw) { try { fs.writeFileSync(PAGES_JSON, outText, 'utf-8'); console.log(`[InjectNavTitle] ✅ 注入 ${injected}, 跳过(已存在) ${skipped}`); } catch (e) { console.error('[InjectNavTitle] ❌ 写回 pages.json 失败:', e.message); return false; } } else { console.log(`[InjectNavTitle] ✓ 无需改动, 跳过(已存在) ${skipped}`); } return true; } if (require.main === module) { run(); } module.exports = { run, netBraces, findInsertIdx, addLeadingComma, processStyleBlock };