334 lines
11 KiB
JavaScript
334 lines
11 KiB
JavaScript
/**
|
|
* Build-time: 按需注入 diy componentPlaceholder 占位
|
|
*
|
|
* 设计原则: 局部文本替换, 不破坏 pages.json 原有结构
|
|
* ============================================================
|
|
* - 不调用 JSON.parse / JSON.stringify (它们会丢失注释和预处理指令)
|
|
* - 不剥离 // 注释
|
|
* - 不剥离 // #ifdef / // #ifndef / // #endif 等 uni-app 预处理指令
|
|
* - 只对每个 page 的 "style": { ... } 块做局部修改
|
|
* - 块外的所有内容 (注释, 预处理, 缩进, 换行) 保持原样
|
|
*
|
|
* 工作流 (按需注入, 避免 vendor.js 膨胀):
|
|
* 1. 扫描 components-diy/ 目录, 收集所有 diy-*.vue 组件名 (单一数据源: 文件系统)
|
|
* 2. 逐行扫描 pages.json, 跟踪每个 page 的完整路径:
|
|
* - 顶层 pages[] 里的 page: 路径 = path (如 "pages/index/index")
|
|
* - subPackages[] 里的 page: 路径 = root + "/" + path (如 "pages_goods/category")
|
|
* 3. 对每个 "style": { ... } 块:
|
|
* a) 读取对应 .vue 文件, 检查是否包含 <diy- 引用
|
|
* b) **只有真正用 diy 组件的 page** 才注入/保留 componentPlaceholder
|
|
* c) 不用的 page: 若已有 placeholder 则移除 (含 _diy_placeholder 标记)
|
|
* d) 用的 page:
|
|
* - 已有 componentPlaceholder → 跳过
|
|
* - 有 _diy_placeholder 标记 → 展开为完整块
|
|
* - 都没有 → 注入 _diy_placeholder 标记 (带逗号处理)
|
|
* 4. 仅在文件有变化时写回
|
|
*
|
|
* 为什么按需注入: 给 113 个 page 全注入 54 项 placeholder, uni-app 会把所有 54 个
|
|
* diy 组件名打进 vendor.js, 每个出现 226 次, 让 vendor.js 膨胀 ~270KB.
|
|
* 只对真正用 <diy-*> 的 24 个 page 注入, vendor.js 维持 ~570KB, 主包减少 ~265KB.
|
|
*
|
|
* 调用时机: vue.config.js 顶部 (module.exports 之前) require 此文件自动执行.
|
|
*/
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
const PAGES_JSON = path.join(PROJECT_ROOT, 'pages.json');
|
|
const DIY_DIR = path.join(PROJECT_ROOT, 'components-diy');
|
|
|
|
/**
|
|
* 扫描 components-diy/ 目录, 收集所有 diy-* 组件名.
|
|
* - 只匹配顶层 diy-*.vue 文件 (uni-app easycom 单文件组件)
|
|
* - 排除子目录 (css/ js/ placeholder/) 和其它文件 (minx.js 等)
|
|
* - 单一数据源: 文件系统, 无需手维护清单
|
|
*/
|
|
function collectDiyComponents() {
|
|
if (!fs.existsSync(DIY_DIR)) {
|
|
console.warn(`[InjectPlaceholder] ⚠️ ${DIY_DIR} 不存在, diy 组件清单为空`);
|
|
return [];
|
|
}
|
|
const entries = fs.readdirSync(DIY_DIR, { withFileTypes: true });
|
|
const names = [];
|
|
for (const e of entries) {
|
|
if (e.isFile() && e.name.startsWith('diy-') && e.name.endsWith('.vue')) {
|
|
names.push(e.name.slice(0, -'.vue'.length));
|
|
}
|
|
}
|
|
names.sort();
|
|
return names;
|
|
}
|
|
|
|
const DIY_LIST = collectDiyComponents();
|
|
|
|
/**
|
|
* 字符串感知的花括号计数.
|
|
* 跳过 // 行注释和 "" 字符串内的花括号.
|
|
* 返回该行净增的花括号深度.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 在 body 中找最后一个 "有意义的" 行 (非空, 非纯注释).
|
|
*/
|
|
function lastMeaningfulIdx(bodyLines) {
|
|
for (let j = bodyLines.length - 1; j >= 0; j--) {
|
|
if (!/^\s*(\/\/.*)?$/.test(bodyLines[j])) return j;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* 检查 page 的 .vue 文件是否引用了 <diy-*> 组件.
|
|
* 简单的字符串检查, 包含 <diy- 即认为使用 (覆盖模板引用、动态组件等).
|
|
*/
|
|
function pageUsesDiy(fullPagePath) {
|
|
if (!fullPagePath) return false;
|
|
const vuePath = path.join(PROJECT_ROOT, fullPagePath + '.vue');
|
|
try {
|
|
const content = fs.readFileSync(vuePath, 'utf-8');
|
|
return /<diy-/.test(content);
|
|
} catch (e) {
|
|
return false; // 文件不存在时不注入
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从 body 中移除 componentPlaceholder 块和 _diy_placeholder 标记.
|
|
* 用于: page 之前注入了 placeholder, 但重构后不再使用 diy 组件.
|
|
* 自动修正尾逗号, 保证移除后 JSON 仍合法.
|
|
*/
|
|
function stripPlaceholderFromBody(bodyLines) {
|
|
let startIdx = -1, endIdx = -1, braceDepth = 0;
|
|
for (let i = 0; i < bodyLines.length; i++) {
|
|
const line = bodyLines[i];
|
|
if (startIdx === -1) {
|
|
const m = line.match(/^\s*"componentPlaceholder"\s*:\s*\{/);
|
|
if (m) {
|
|
startIdx = i;
|
|
const idx = line.indexOf('{', m.index);
|
|
for (let j = idx; j < line.length; j++) {
|
|
const c = line[j];
|
|
if (c === '{') braceDepth++;
|
|
else if (c === '}') braceDepth--;
|
|
}
|
|
if (braceDepth === 0) endIdx = i;
|
|
}
|
|
} else {
|
|
for (const c of line) {
|
|
if (c === '{') braceDepth++;
|
|
else if (c === '}') braceDepth--;
|
|
}
|
|
if (braceDepth === 0) { endIdx = i; break; }
|
|
}
|
|
}
|
|
const toRemove = new Set();
|
|
if (startIdx >= 0 && endIdx >= 0) {
|
|
for (let i = startIdx; i <= endIdx; i++) toRemove.add(i);
|
|
}
|
|
for (let i = 0; i < bodyLines.length; i++) {
|
|
if (/^\s*"_diy_placeholder"\s*:\s*true/.test(bodyLines[i])) toRemove.add(i);
|
|
}
|
|
if (toRemove.size === 0) return bodyLines;
|
|
|
|
const newBody = [];
|
|
for (let i = 0; i < bodyLines.length; i++) {
|
|
if (toRemove.has(i)) continue;
|
|
newBody.push(bodyLines[i]);
|
|
}
|
|
// 修正: 移除后, 新的最后有意义行不应有尾逗号
|
|
const lastIdx = lastMeaningfulIdx(newBody);
|
|
if (lastIdx >= 0) {
|
|
const line = newBody[lastIdx];
|
|
const commentMatch = line.match(/(\/\/.*)$/);
|
|
const comment = commentMatch ? commentMatch[1] : '';
|
|
const valuePart = line.substring(0, line.length - comment.length);
|
|
if (valuePart.trimEnd().endsWith(',')) {
|
|
newBody[lastIdx] = valuePart.trimEnd().replace(/,\s*$/, '') + comment;
|
|
}
|
|
}
|
|
return newBody;
|
|
}
|
|
|
|
/**
|
|
* 处理单个 style 块.
|
|
* needsPlaceholder=true: 注入/展开/保留 componentPlaceholder
|
|
* needsPlaceholder=false: 若已有 placeholder 则移除
|
|
* 返回 { lines, action } action: 'expand' | 'inject-marker' | 'remove' | 'skip' | 'no-change'
|
|
*/
|
|
function processStyleBlock(openLine, bodyLines, closeLine, needsPlaceholder) {
|
|
const styleIndent = (openLine.match(/^(\s*)/) || ['', ''])[1];
|
|
const innerIndent = styleIndent + '\t';
|
|
|
|
// 不需要 placeholder: 移除已有
|
|
if (!needsPlaceholder) {
|
|
const newBody = stripPlaceholderFromBody(bodyLines);
|
|
if (newBody.length === bodyLines.length) {
|
|
return { lines: [openLine, ...bodyLines, closeLine], action: 'no-change' };
|
|
}
|
|
return { lines: [openLine, ...newBody, closeLine], action: 'remove' };
|
|
}
|
|
|
|
// 需要 placeholder: 已存在 → 跳过
|
|
if (bodyLines.some(l => /^\s*"componentPlaceholder"\s*:/.test(l))) {
|
|
return { lines: [openLine, ...bodyLines, closeLine], action: 'skip' };
|
|
}
|
|
|
|
// 找 _diy_placeholder 标记, 展开
|
|
const markerIdx = bodyLines.findIndex(l => /^\s*"_diy_placeholder"\s*:\s*true/.test(l));
|
|
if (markerIdx >= 0) {
|
|
const tailCommaMatch = bodyLines[markerIdx].match(/^\s*"_diy_placeholder"\s*:\s*true\s*(,?)/);
|
|
const hasTailComma = tailCommaMatch && tailCommaMatch[1] === ',';
|
|
const newBody = [];
|
|
for (let j = 0; j < bodyLines.length; j++) {
|
|
if (j === markerIdx) {
|
|
newBody.push(`${innerIndent}"componentPlaceholder": {`);
|
|
// JSON 不允许尾逗号, 最后一个 diy 条目不能加 ","
|
|
for (let i = 0; i < DIY_LIST.length; i++) {
|
|
const name = DIY_LIST[i];
|
|
const isLast = i === DIY_LIST.length - 1;
|
|
newBody.push(`${innerIndent}\t"${name}": "view"${isLast ? '' : ','}`);
|
|
}
|
|
newBody.push(`${innerIndent}}` + (hasTailComma ? ',' : ''));
|
|
} else {
|
|
newBody.push(bodyLines[j]);
|
|
}
|
|
}
|
|
return { lines: [openLine, ...newBody, closeLine], action: 'expand' };
|
|
}
|
|
|
|
// 注入标记
|
|
const newBody = bodyLines.slice();
|
|
const lastIdx = lastMeaningfulIdx(newBody);
|
|
if (lastIdx === -1) {
|
|
newBody.push(`${innerIndent}"_diy_placeholder": true`);
|
|
} else {
|
|
const lastLine = newBody[lastIdx];
|
|
const commentMatch = lastLine.match(/(\/\/.*)$/);
|
|
const comment = commentMatch ? commentMatch[1] : '';
|
|
const valuePart = lastLine.substring(0, lastLine.length - comment.length);
|
|
if (!valuePart.trimEnd().endsWith(',')) {
|
|
newBody[lastIdx] = valuePart.trimEnd() + ',' + (comment ? ' ' + comment.trimStart() : '');
|
|
}
|
|
newBody.push(`${innerIndent}"_diy_placeholder": true`);
|
|
}
|
|
return { lines: [openLine, ...newBody, closeLine], action: 'inject-marker' };
|
|
}
|
|
|
|
function run() {
|
|
let raw;
|
|
try {
|
|
raw = fs.readFileSync(PAGES_JSON, 'utf-8');
|
|
} catch (e) {
|
|
console.error('[InjectPlaceholder] ❌ 读取 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 currentSubRoot = null; // 当前 subPackage 的 root, 顶层 page 时为 null
|
|
let currentPath = null; // 当前 page 的 path
|
|
let expanded = 0, added = 0, removed = 0, skipped = 0, noChange = 0;
|
|
|
|
while (i < lines.length) {
|
|
const line = lines[i];
|
|
|
|
// 跟踪 subPackage root
|
|
const rootMatch = line.match(/^\s*"root"\s*:\s*"([^"]+)"/);
|
|
if (rootMatch) {
|
|
currentSubRoot = rootMatch[1];
|
|
out.push(line);
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// 跟踪 page path
|
|
const pathMatch = line.match(/^\s*"path"\s*:\s*"([^"]+)"/);
|
|
if (pathMatch) {
|
|
currentPath = pathMatch[1];
|
|
out.push(line);
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// 处理 style 块
|
|
const styleMatch = line.match(/^(\s*)"style"\s*:\s*\{\s*,?\s*$/);
|
|
if (!styleMatch) {
|
|
out.push(line);
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
const openLine = line;
|
|
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);
|
|
|
|
// 关键: 按需判断. 读取 .vue 文件看是否真的用了 <diy-*>.
|
|
const fullPagePath = currentSubRoot
|
|
? `${currentSubRoot}/${currentPath}`
|
|
: currentPath;
|
|
const needsPlaceholder = pageUsesDiy(fullPagePath);
|
|
|
|
const result = processStyleBlock(openLine, bodyLines, closeLine, needsPlaceholder);
|
|
for (const l of result.lines) out.push(l);
|
|
if (result.action === 'expand') expanded++;
|
|
else if (result.action === 'inject-marker') added++;
|
|
else if (result.action === 'remove') removed++;
|
|
else if (result.action === 'skip') skipped++;
|
|
else if (result.action === 'no-change') noChange++;
|
|
}
|
|
|
|
const outText = out.join(EOL);
|
|
if (outText !== raw) {
|
|
try {
|
|
fs.writeFileSync(PAGES_JSON, outText, 'utf-8');
|
|
console.log(`[InjectPlaceholder] ✅ 展开 ${expanded}, 注入 ${added}, 移除 ${removed}, 跳过 ${skipped}, 无变化 ${noChange}, diy 组件 ${DIY_LIST.length}`);
|
|
} catch (e) {
|
|
console.error('[InjectPlaceholder] ❌ 写回 pages.json 失败:', e.message);
|
|
return false;
|
|
}
|
|
} else {
|
|
console.log(`[InjectPlaceholder] ✓ 无需改动, diy 组件 ${DIY_LIST.length}`);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
run();
|