feat(extension): 缩减配置注入并输出 menav-config.json
- 构建:生成 dist/menav-config.json 供扩展按需加载 - 页面:menav-config-data 仅注入扩展元信息(configUrl/pageTemplates/icons/homePageId/allowedSchemes) - 运行时:模板判定优先 pageTemplates,兼容旧 cfg.data[pageId].template - 文档:更新 README 的扩展接口说明
This commit is contained in:
@@ -207,7 +207,7 @@ icons:
|
|||||||
|
|
||||||
**1. MeNav 浏览器扩展支持接口**
|
**1. MeNav 浏览器扩展支持接口**
|
||||||
|
|
||||||
- 注入序列化的配置数据供扩展读取(`configJSON`)
|
- 注入扩展元信息(`menav-config-data`)并输出 `dist/menav-config.json` 供扩展按需加载(避免把整站配置注入到 `index.html`)
|
||||||
- 暴露 `window.MeNav` 基础能力与 DOM 数据属性,支持元素精准定位与更新
|
- 暴露 `window.MeNav` 基础能力与 DOM 数据属性,支持元素精准定位与更新
|
||||||
- 为扩展推送与页面联动打通基础能力
|
- 为扩展推送与页面联动打通基础能力
|
||||||
|
|
||||||
|
|||||||
108
src/generator.js
108
src/generator.js
@@ -7,6 +7,9 @@ const Handlebars = require('handlebars');
|
|||||||
// 导入Handlebars助手函数
|
// 导入Handlebars助手函数
|
||||||
const { registerAllHelpers } = require('./helpers');
|
const { registerAllHelpers } = require('./helpers');
|
||||||
|
|
||||||
|
// 扩展配置输出:独立静态文件(按需加载)
|
||||||
|
const MENAV_EXTENSION_CONFIG_FILE = 'menav-config.json';
|
||||||
|
|
||||||
// 注册Handlebars实例和辅助函数
|
// 注册Handlebars实例和辅助函数
|
||||||
const handlebars = Handlebars.create();
|
const handlebars = Handlebars.create();
|
||||||
registerAllHelpers(handlebars);
|
registerAllHelpers(handlebars);
|
||||||
@@ -27,11 +30,11 @@ function loadHandlebarsTemplates() {
|
|||||||
.filter((file) => file.endsWith('.hbs'))
|
.filter((file) => file.endsWith('.hbs'))
|
||||||
.sort()
|
.sort()
|
||||||
.forEach((file) => {
|
.forEach((file) => {
|
||||||
const layoutName = path.basename(file, '.hbs');
|
const layoutName = path.basename(file, '.hbs');
|
||||||
const layoutPath = path.join(layoutsDir, file);
|
const layoutPath = path.join(layoutsDir, file);
|
||||||
const layoutContent = fs.readFileSync(layoutPath, 'utf8');
|
const layoutContent = fs.readFileSync(layoutPath, 'utf8');
|
||||||
handlebars.registerPartial(layoutName, layoutContent);
|
handlebars.registerPartial(layoutName, layoutContent);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Layouts directory not found. Cannot proceed without layout templates.');
|
throw new Error('Layouts directory not found. Cannot proceed without layout templates.');
|
||||||
}
|
}
|
||||||
@@ -43,11 +46,11 @@ function loadHandlebarsTemplates() {
|
|||||||
.filter((file) => file.endsWith('.hbs'))
|
.filter((file) => file.endsWith('.hbs'))
|
||||||
.sort()
|
.sort()
|
||||||
.forEach((file) => {
|
.forEach((file) => {
|
||||||
const componentName = path.basename(file, '.hbs');
|
const componentName = path.basename(file, '.hbs');
|
||||||
const componentPath = path.join(componentsDir, file);
|
const componentPath = path.join(componentsDir, file);
|
||||||
const componentContent = fs.readFileSync(componentPath, 'utf8');
|
const componentContent = fs.readFileSync(componentPath, 'utf8');
|
||||||
handlebars.registerPartial(componentName, componentContent);
|
handlebars.registerPartial(componentName, componentContent);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Components directory not found. Cannot proceed without component templates.');
|
throw new Error('Components directory not found. Cannot proceed without component templates.');
|
||||||
}
|
}
|
||||||
@@ -839,6 +842,68 @@ function applyRepoMetaToCategories(categories, repoMetaMap) {
|
|||||||
categories.forEach(walk);
|
categories.forEach(walk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算页面实际使用的模板名(与 renderPage 的规则保持一致)
|
||||||
|
* @param {string} pageId 页面ID
|
||||||
|
* @param {Object} config 已标准化的配置(prepareRenderData 的 renderData)
|
||||||
|
* @returns {string} 模板名
|
||||||
|
*/
|
||||||
|
function resolveTemplateNameForPage(pageId, config) {
|
||||||
|
if (!pageId) return 'page';
|
||||||
|
|
||||||
|
const pageConfig = config && config[pageId] ? config[pageId] : null;
|
||||||
|
const explicitTemplate =
|
||||||
|
pageConfig && typeof pageConfig.template === 'string' ? pageConfig.template.trim() : '';
|
||||||
|
if (explicitTemplate) return explicitTemplate;
|
||||||
|
|
||||||
|
const inferredTemplatePath = path.join(process.cwd(), 'templates', 'pages', `${pageId}.hbs`);
|
||||||
|
if (fs.existsSync(inferredTemplatePath)) return pageId;
|
||||||
|
|
||||||
|
return 'page';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建“扩展专用配置”(避免把整站配置/书签数据注入到 index.html)
|
||||||
|
* - 页面内仅注入必要元信息(版本/配置 URL/少量运行时参数)
|
||||||
|
* - 扩展如需更多信息,可按需请求 dist/menav-config.json
|
||||||
|
* @param {Object} renderData prepareRenderData 生成的渲染数据
|
||||||
|
* @returns {Object} 扩展配置对象(可直接 JSON.stringify 输出到文件)
|
||||||
|
*/
|
||||||
|
function buildExtensionConfig(renderData) {
|
||||||
|
const version =
|
||||||
|
(renderData && renderData._meta && renderData._meta.version) ||
|
||||||
|
process.env.npm_package_version ||
|
||||||
|
'1.0.0';
|
||||||
|
|
||||||
|
const pageTemplates = {};
|
||||||
|
if (Array.isArray(renderData && renderData.navigation)) {
|
||||||
|
renderData.navigation.forEach((navItem) => {
|
||||||
|
const pageId = navItem && navItem.id ? String(navItem.id).trim() : '';
|
||||||
|
if (!pageId) return;
|
||||||
|
pageTemplates[pageId] = resolveTemplateNameForPage(pageId, renderData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedSchemes =
|
||||||
|
renderData &&
|
||||||
|
renderData.site &&
|
||||||
|
renderData.site.security &&
|
||||||
|
Array.isArray(renderData.site.security.allowedSchemes)
|
||||||
|
? renderData.site.security.allowedSchemes
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
icons: renderData && renderData.icons ? renderData.icons : undefined,
|
||||||
|
data: {
|
||||||
|
homePageId: renderData && renderData.homePageId ? renderData.homePageId : null,
|
||||||
|
pageTemplates,
|
||||||
|
site: allowedSchemes ? { security: { allowedSchemes } } : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 准备渲染数据,添加模板所需的特殊属性
|
* 准备渲染数据,添加模板所需的特殊属性
|
||||||
* @param {Object} config 配置对象
|
* @param {Object} config 配置对象
|
||||||
@@ -895,12 +960,16 @@ function prepareRenderData(config) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加序列化的配置数据,用于浏览器扩展(确保包含 homePageId 等处理结果)
|
// P1-7:避免把整站配置(尤其 pages/categories/sites)注入到 index.html,减少体积并明确前端暴露边界
|
||||||
|
// - dist/menav-config.json:扩展专用配置(按需加载)
|
||||||
|
// - menav-config-data:仅注入必要元信息(版本/配置 URL/少量运行时参数)
|
||||||
|
const extensionConfig = buildExtensionConfig(renderData);
|
||||||
|
renderData.extensionConfig = extensionConfig;
|
||||||
|
renderData.extensionConfigUrl = `./${MENAV_EXTENSION_CONFIG_FILE}`;
|
||||||
renderData.configJSON = makeJsonSafeForHtmlScript(
|
renderData.configJSON = makeJsonSafeForHtmlScript(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
version: process.env.npm_package_version || '1.0.0',
|
...extensionConfig,
|
||||||
timestamp: new Date().toISOString(),
|
configUrl: renderData.extensionConfigUrl,
|
||||||
data: renderData, // 使用经过处理的renderData而不是原始config
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1852,6 +1921,17 @@ function main() {
|
|||||||
// 生成HTML
|
// 生成HTML
|
||||||
fs.writeFileSync('dist/index.html', htmlContent);
|
fs.writeFileSync('dist/index.html', htmlContent);
|
||||||
|
|
||||||
|
// 扩展专用配置:独立静态文件(按需加载)
|
||||||
|
try {
|
||||||
|
const extensionConfig =
|
||||||
|
config && config.extensionConfig ? JSON.stringify(config.extensionConfig, null, 2) : '';
|
||||||
|
if (extensionConfig) {
|
||||||
|
fs.writeFileSync(path.join('dist', MENAV_EXTENSION_CONFIG_FILE), extensionConfig);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error writing extension config file:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// GitHub Pages 静态路由回退:用于支持 /<id> 形式的路径深链接
|
// GitHub Pages 静态路由回退:用于支持 /<id> 形式的路径深链接
|
||||||
fs.writeFileSync('dist/404.html', generate404Html(config));
|
fs.writeFileSync('dist/404.html', generate404Html(config));
|
||||||
|
|
||||||
|
|||||||
@@ -399,9 +399,27 @@ window.MeNav = {
|
|||||||
window.MeNav && typeof window.MeNav.getConfig === 'function'
|
window.MeNav && typeof window.MeNav.getConfig === 'function'
|
||||||
? window.MeNav.getConfig()
|
? window.MeNav.getConfig()
|
||||||
: null;
|
: null;
|
||||||
const pageConfig = cfg && cfg.data && pageId ? cfg.data[pageId] : null;
|
const pageTemplates =
|
||||||
if (pageConfig && pageConfig.template) {
|
cfg && cfg.data && cfg.data.pageTemplates && typeof cfg.data.pageTemplates === 'object'
|
||||||
templateName = String(pageConfig.template);
|
? cfg.data.pageTemplates
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const templateFromMap =
|
||||||
|
pageTemplates && pageId && pageTemplates[pageId]
|
||||||
|
? String(pageTemplates[pageId]).trim()
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// 兼容旧版:cfg.data[pageId].template
|
||||||
|
const legacyPageConfig = cfg && cfg.data && pageId ? cfg.data[pageId] : null;
|
||||||
|
const templateFromLegacy =
|
||||||
|
legacyPageConfig && legacyPageConfig.template
|
||||||
|
? String(legacyPageConfig.template).trim()
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if (templateFromMap) {
|
||||||
|
templateName = templateFromMap;
|
||||||
|
} else if (templateFromLegacy) {
|
||||||
|
templateName = templateFromLegacy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// projects 模板使用代码仓库风格卡片(与生成端 templates/components/site-card.hbs 保持一致)
|
// projects 模板使用代码仓库风格卡片(与生成端 templates/components/site-card.hbs 保持一致)
|
||||||
|
|||||||
@@ -190,7 +190,7 @@
|
|||||||
<i class="fas fa-moon"></i>
|
<i class="fas fa-moon"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 配置数据 - 用于浏览器扩展 -->
|
<!-- 配置元信息(版本/配置URL/少量运行时参数)- 用于浏览器扩展按需加载 -->
|
||||||
<script id="menav-config-data" type="application/json" style="display: none;">
|
<script id="menav-config-data" type="application/json" style="display: none;">
|
||||||
{{{configJSON}}}
|
{{{configJSON}}}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
56
test/p1-7-extension-config-minimal.node-test.js
Normal file
56
test/p1-7-extension-config-minimal.node-test.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const { loadHandlebarsTemplates, loadConfig, generateHTML } = require('../src/generator.js');
|
||||||
|
|
||||||
|
function withRepoRoot(fn) {
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
process.chdir(path.join(__dirname, '..'));
|
||||||
|
try {
|
||||||
|
return fn();
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('P1-7:页面内不应注入整站 configJSON,应仅保留扩展元信息与最小运行时参数', () => {
|
||||||
|
withRepoRoot(() => {
|
||||||
|
loadHandlebarsTemplates();
|
||||||
|
const config = loadConfig();
|
||||||
|
const html = generateHTML(config);
|
||||||
|
|
||||||
|
const match = html.match(
|
||||||
|
/<script id="menav-config-data"[^>]*>([\s\S]*?)<\/script>/m
|
||||||
|
);
|
||||||
|
assert.ok(match, '应输出 menav-config-data 脚本块');
|
||||||
|
|
||||||
|
const raw = String(match[1] || '').trim();
|
||||||
|
assert.ok(raw.length > 0, 'menav-config-data 内容不应为空');
|
||||||
|
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
|
||||||
|
assert.ok(parsed && typeof parsed === 'object');
|
||||||
|
assert.equal(parsed.configUrl, './menav-config.json');
|
||||||
|
assert.ok(parsed.version, '应包含 version');
|
||||||
|
assert.ok(parsed.timestamp, '应包含 timestamp');
|
||||||
|
|
||||||
|
assert.ok(parsed.icons && typeof parsed.icons === 'object', '应包含 icons 配置(用于运行时)');
|
||||||
|
|
||||||
|
assert.ok(parsed.data && typeof parsed.data === 'object', '应包含 data 对象');
|
||||||
|
assert.ok(parsed.data.homePageId, '应包含 homePageId');
|
||||||
|
assert.ok(
|
||||||
|
parsed.data.pageTemplates && typeof parsed.data.pageTemplates === 'object',
|
||||||
|
'应包含 pageTemplates'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 不应再把 pages/<id>.yml 的完整结构(categories/sites 等)注入到页面中
|
||||||
|
assert.equal(parsed.data.common, undefined);
|
||||||
|
assert.equal(parsed.data.projects, undefined);
|
||||||
|
assert.equal(parsed.data.articles, undefined);
|
||||||
|
assert.equal(parsed.data.bookmarks, undefined);
|
||||||
|
assert.ok(!/"categories"\s*:/.test(raw), '不应包含 categories 字段');
|
||||||
|
assert.ok(!/"sites"\s*:/.test(raw), '不应包含 sites 字段');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user