refactor: 模块化重构 generator 和 runtime
This commit is contained in:
17
src/generator/utils/html.js
Normal file
17
src/generator/utils/html.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// HTML 转义函数,防止 XSS 攻击
|
||||
function escapeHtml(unsafe) {
|
||||
if (unsafe === undefined || unsafe === null) {
|
||||
return '';
|
||||
}
|
||||
return String(unsafe)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
escapeHtml,
|
||||
};
|
||||
|
||||
101
src/generator/utils/pageMeta.js
Normal file
101
src/generator/utils/pageMeta.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
/**
|
||||
* 解析页面配置文件路径(优先 user,回退 _default)
|
||||
* 注意:仅用于构建期读取文件元信息,不会把路径注入到页面/扩展配置中。
|
||||
* @param {string} pageId 页面ID(与 pages/<id>.yml 文件名对应)
|
||||
* @returns {string|null} 文件路径或 null
|
||||
*/
|
||||
function resolvePageConfigFilePath(pageId) {
|
||||
if (!pageId) return null;
|
||||
|
||||
const candidates = [
|
||||
path.join(process.cwd(), 'config', 'user', 'pages', `${pageId}.yml`),
|
||||
path.join(process.cwd(), 'config', 'user', 'pages', `${pageId}.yaml`),
|
||||
path.join(process.cwd(), 'config', '_default', 'pages', `${pageId}.yml`),
|
||||
path.join(process.cwd(), 'config', '_default', 'pages', `${pageId}.yaml`),
|
||||
];
|
||||
|
||||
for (const filePath of candidates) {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) return filePath;
|
||||
} catch (e) {
|
||||
// 忽略 IO 异常,继续尝试下一个候选
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取文件最后一次 git 提交时间(ISO 字符串)
|
||||
* @param {string} filePath 文件路径
|
||||
* @returns {string|null} ISO 字符串(UTC),失败返回 null
|
||||
*/
|
||||
function tryGetGitLastCommitIso(filePath) {
|
||||
if (!filePath) return null;
|
||||
|
||||
try {
|
||||
const relativePath = path.relative(process.cwd(), filePath).replace(/\\/g, '/');
|
||||
const output = execFileSync('git', ['log', '-1', '--format=%cI', '--', relativePath], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
const raw = String(output || '').trim();
|
||||
if (!raw) return null;
|
||||
|
||||
const date = new Date(raw);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
|
||||
return date.toISOString();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件 mtime(ISO 字符串)
|
||||
* @param {string} filePath 文件路径
|
||||
* @returns {string|null} ISO 字符串(UTC),失败返回 null
|
||||
*/
|
||||
function tryGetFileMtimeIso(filePath) {
|
||||
if (!filePath) return null;
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
const mtime = stats && stats.mtime ? stats.mtime : null;
|
||||
if (!(mtime instanceof Date) || Number.isNaN(mtime.getTime())) return null;
|
||||
return mtime.toISOString();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算页面配置文件“内容更新时间”(优先 git,回退 mtime)
|
||||
* @param {string} pageId 页面ID
|
||||
* @returns {{updatedAt: string, updatedAtSource: 'git'|'mtime'}|null}
|
||||
*/
|
||||
function getPageConfigUpdatedAtMeta(pageId) {
|
||||
const filePath = resolvePageConfigFilePath(pageId);
|
||||
if (!filePath) return null;
|
||||
|
||||
const gitIso = tryGetGitLastCommitIso(filePath);
|
||||
if (gitIso) {
|
||||
return { updatedAt: gitIso, updatedAtSource: 'git' };
|
||||
}
|
||||
|
||||
const mtimeIso = tryGetFileMtimeIso(filePath);
|
||||
if (mtimeIso) {
|
||||
return { updatedAt: mtimeIso, updatedAtSource: 'mtime' };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPageConfigUpdatedAtMeta,
|
||||
};
|
||||
|
||||
35
src/generator/utils/sites.js
Normal file
35
src/generator/utils/sites.js
Normal file
@@ -0,0 +1,35 @@
|
||||
function normalizeUrlKey(input) {
|
||||
if (!input) return '';
|
||||
try {
|
||||
const u = new URL(String(input));
|
||||
const origin = u.origin;
|
||||
let pathname = u.pathname || '/';
|
||||
// 统一去掉末尾斜杠(根路径除外),避免 https://a.com 与 https://a.com/ 不匹配
|
||||
if (pathname !== '/' && pathname.endsWith('/')) pathname = pathname.slice(0, -1);
|
||||
return `${origin}${pathname}`;
|
||||
} catch {
|
||||
return String(input).trim();
|
||||
}
|
||||
}
|
||||
|
||||
function collectSitesRecursively(node, output) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
|
||||
if (Array.isArray(node.subcategories))
|
||||
node.subcategories.forEach((child) => collectSitesRecursively(child, output));
|
||||
if (Array.isArray(node.groups)) node.groups.forEach((child) => collectSitesRecursively(child, output));
|
||||
if (Array.isArray(node.subgroups))
|
||||
node.subgroups.forEach((child) => collectSitesRecursively(child, output));
|
||||
|
||||
if (Array.isArray(node.sites)) {
|
||||
node.sites.forEach((site) => {
|
||||
if (site && typeof site === 'object') output.push(site);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeUrlKey,
|
||||
collectSitesRecursively,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user